Hegwin.Me

疏影横斜水清浅,暗香浮动月黄昏。

从手动到自动,部署 Ruby 代码到 AWS Lambda

How to Deploy Ruby Code on AWS Lambda: A Brief Guide from Manual to Automated

最近做了一些新的尝试,将一部分与项目主业务关联性不大的功能,单独作为 AWS Lambda 去开发和部署——也算是在 serverless 开发的大潮流中向前迈进了一步。这篇文章主要是基于我在部署 Ruby 代码到 AWS Lambda 过程中,我所学习到的一些方法和心得。

目前 AWS Lambda 支持的语言有 Node, Python, Java, .NET, Ruby 和 Go(以custom runtime的形式)。我在这次尝试中选择了使用 Ruby 作为 Lambda 开发语言,尽管它不算主流,但是我主要是考虑了这些原因:

  1. AWS Lambda 支持 Ruby runtime;
  2. 我对 Ruby 比较熟悉,开发效率更快;
  3. Ruby 的测试框架 RSpec 易读易写,做单元测试比较容易。

当然,这篇文章不是讲我为什么要选择Ruby的,所以不展开讲了。之所以提这部分,是因为语言的选择,对部署的流程也是有一点点影响的。

说到部署 Ruby lambda ,我第一反应是我们会遇到这些问题:

  1. Ruby 项目怎么打包,特别是存在 C extension 依赖的时候
  2. 部署到AWS时,Permission 这一步怎么操作

下面介绍下我所了解到的部署方法,我也会穿插着讲述我怎么解决上面的问题:

  1. Lambda Console
  2. Zip and AWS CLI
  3. Serverless Framework
  4. Jets Framework
  5. CI/CD Pipeline

Lambda Console

lambda-console.jpg

AWS Lambda Console 自然是最简单的方式,在修改文件后直接点击 Deploy 既可以完成部署。当然,还是建议在点击 Deploy 之前进行一些测试。

这个方法只适合非常简单的 lambda,一旦涉及多个文件和依赖的情况,则不推荐这个方式。另外,当代码大小超过 3 MB 时,根据 AWS 的限制,这个 console editor 也是不可使用的。

在这个界面的右上角,我们还可以看到一个“Upload from”的按钮,这便是可以讲到下一个部署方式。

Zip and AWS CLI

上面一部分讲到我们可以通过点击“Upload from”来部署 lambda ,那么这里就简单讲下怎么操作。

假设我们的项目结构是这样,当然这也是一个常规的 Ruby lambda 的目录结构了:

lambda-demo
│   .gitignore
│   Gemfile
│   Gemfile.lock
│   README.md
│   lambda.rb 
└───lib
└───spec
└───vendor

我们可以像写 ruby gem的方式去组织 lambda 的文件结构,下列文件是必须在部署时打包好的:

  • lambda.rb 是 lambda 的入口文件,这个文件不一定要叫这个名字,只要文件名和其中定义的方法名和配置到lambda 的 runtime settings 里一致即可
  • lib 里会有多个文件或者目录,是 lambda 具体实现的存放位置
  • vendor 是存放依赖的 gems 的地方,由于 lambda 执行时只有最基础的 ruby runtime,这些 gem 也是需要要打包进 lambda 一起部署的(除非单独部署依赖)

其他的文件或者目录,如 REAMD 和 spec,并非 lambda 运行所需,无需打包部署。

打包的命令也比较简单:

$ bundle install --standalone --path vendor/bundle
$ zip -r lambda.zip vendor lib lambda.rb

这样我们就有了一个打包好的 lambda.zip 文件,接下来会有两个选择:

  1. 手动 Lambda Console,点击 Upload from 按钮,选择 “.zip file” 方式,再选择刚才的 zip 文件即可;
  2. 利用 AWS CLI 来完成上传和部署,命令如下(前提是你已经在本地配置好 AWS 并具有权限):
$ aws lambda update-function-code --function-name myFunction \
  --zip-file fileb://lambda.zip

AWS 也支持我们先上传 zip 文件到 S3,再从S3去进行部署。

更具体的方面可以查看官方的文档:Working with .zip file archives for Ruby Lambda functions

Serverless Framework

是否觉得自己手动打包和部署很麻烦?Ruby on Rails 开发者经常用到的一个部署工具是 capistrano,在完成配置之后,每次部署到服务器,只需要执行一个命令 cap production deploy ,即可完成上传、安装依赖、编译、重启服务器等等一系列步骤。那么 lambda 部署是否有这样的工具呢?答案就是 serverless

serverless 是一套针对 AWS Lambda 的完整框架,从新建项目和配置到部署都有涵盖,也支持各种语言,非常便利。

serverless 依赖 node.js,再确认自己有 node.js 环境后,只需这样即可完成安装:

npm install -g serverless

安装完成后,我们就可以使用 serverless 命令,或者缩略版的 sls 的命令了,建议先执行下 serverless help看看这个工具都能干些什么。

在新建 lambda 项目的时候, serverless 允许我们从一些模板去进行创建,serverless 的社区提供了很多 examples,免去了自己组织项目的麻烦,也有可用的代码,虽然基础但也很强大了,比如:

比如我们要新建一个 LINE bot 的话,只要这样:

$ serverless --template-url=https://github.com/serverless/examples/tree/v3/aws-ruby-line-bot

执行时,我们可以看到以下内容:

serverless-create.jpg

在创建时,即会问我是否需要部署,我选择了No。它在 “What next?” 里面给出了上述提示,其中它的部署命令也很简洁,即 sls deploy

当然,现实并没有这么简单,实际上我们需要去项目下 serverless.yml 文件中进行配置。我们从 LINE bot template 获取的 serverless.yml 大概长这样:

service: my-line-bot

frameworkVersion: "3"

provider:
  name: aws
  runtime: ruby2.7

functions:
  webhook:
    handler: handler.webhook
    events:
      - http:
          path: webhook
          method: post

plugins:
  - serverless-hooks-plugin

custom:
  hooks:
    package:initialize:
      - bundle install --deployment

在现实情况里,我们要做的配置可能有不少:比如现在 AWS Lambda 已经不支持 Ruby 2.7了,这就需要将provider.runtime 替换成 ruby3.2 (在2024年4月,其将支持 Ruby 3.3);而 serverless.yml 能配置的项目其实非常多,包括 API Gateway、 ALB、VPC、 SQS都是可以在 serverless.yml 中进行配置的,具体可参见其文档

Serverless 框架非常强大,但具体的使用也要看 DevOps 的情况,如果你的团队是完全依赖 terraform 来管理 AWS 资源的话,那么使用 serverless 的自由度可能会受到限制。

Jets Framework

如果说 Serverless 是针对各种语言、面向对个平台的云函数框架,那么 Jets 或者说 Ruby on Jets 的定位则更像是 Ruby on Rails 的 Lambda (Cloud Function) 版本,官方对其描述为“The Ruby Serverless Framework”:

Ruby on Jets allows you to create and deploy serverless services with ease, and to seamlessly glue AWS services together with the most beautiful dynamic language: Ruby. It includes everything you need to build an API and deploy it to AWS Lambda. Jets leverages the power of Ruby to make serverless joyful for everyone.

简而言之,如果你对 rails 的开发非常熟练,那么你在利用 jets 的时候,将会有种非常熟悉的感觉。

Jets 的 Getting Started 文档非常有意思,作者根据你创建项目的目的,将学习路径分成了下面三个分支:

  1. Job
  2. API
  3. HTML

假设我们现在要走 API 这个路线(这也是 jets 的最佳用例),首先我们需要安装jet。jet 是作为一个 gem 发布的,它的安装和项目创建和 rails 非常相似:

$ gem install jets
$ jets new demo --mode api

      create  
       exist  
      create  .env
      create  .gitignore
      create  .jetsignore
      create  .rspec
      create  Gemfile
      create  README.md
      create  Rakefile
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/javascript/application.js
      create  app/jobs/application_job.rb
      create  app/models/application_record.rb
      create  config.ru
      create  config/application.rb
      create  config/database.yml
      # ...

从它生成的文件来,Jets 的很多概念,还有项目结构,都是 rails 的轻量版,学习曲线很低很平滑。

它的部署也是比较容易的,在配置好 AWS 之后,只要一条命令即可完成部署:

$ jets deploy

jets 的部署本质是也是打包成 zip 文件并上传到 S3,稍微不同的是它利用了 AWS 的 CloudFormation, jets会根据 template 创建一组 CloudFormation stacks,包括 Lambda、API Gateway、PreheatJob 等资源——可以批量操作资源,这也是利用 CloudFormation 的一个优势,此外,它可以帮助做版本控制,允许我们 revert stacks 到某个版本。

Access Control

这里需要注意的一点是,权限问题。我在此前都没有提到权限问题,是因为我假定操作者是在本地部署,且有 AWS Resouces 的全部操作权限。如果账号权限不足,那无论是 AWS CLI 还是 serverless 都无法成功部署。在下文中 CI/CD Pipeline 中,会再一次提及权限控制。

在此处,由于 jets 是利用 CloudFormation 去发布,而相关资源是通过 stack 创建出来的。如果我们的 lambda 需要创建一个 SQS,而 stack 所关联的 Role 需要具备相应权限才能创建 SQS,详见 AWS CloudFormation service role

Gem Layer

jets 的部署过程其实包含了不少“魔法”。Gem Layer 便是其中之一。 jets 把 gems 依赖打包到 Gem Layer 中,如果我们的 Gemfile ——也就是说依赖——没有发生变化,那么 Gem Layer 这一层是无需变化的,只需要更新部署我们自己的代码即可,这其实可以大大减少打包的体积,也可以提高部署速度。

CI/CD Pipeline

严格的说,CI/CD Pipeline 并不是一个独立的部署方法,它只是把上面提到的方式(除了 Lambda Console),进行了自动化。

Workflow for AWS CLI

以 Github actions 为例,假设我们需要运行 rspec 并通过 AWS CLI 来部署,那么一个基础版本的 workflow 可能是这样:

name: Package and Deploy Lambda

on:
  push:
    branches: [ 'main' ]

jobs:
  setup-ruby:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set up Ruby 3.2
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2.2
      - name: Install Bundler
        run: gem install bundler --version 2.3.7

  unit-test:
    name: RSpec
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    needs: setup-ruby
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install dependencies and Run Rspec
        run: |
          bundle install --standalone --path vendor/bundle
          bundle exec rspec spec

  package-and-deploy:
    name: Package + Deploy
    needs: [ 'unit-test' ]
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      packages: write
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Use Ruby and Bundler
        uses: ./.github/actions/setup-ruby
      - name: Package
        run: |
          bundle config set without 'development test'
          bundle install --standalone --path vendor/bundle
          zip -r lambda.zip vendor lib handler.rb
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume:
          role-session-name:
          aws-region:
      - name: Upload lambda to AWS
        env:
          FUNCTION_NAME: arn:aws:lambda:region:id:function:lambda-demo
        run: |
          aws lambda update-function-code --function-name $FUNCTION_NAME --zip-file fileb://${{ github.workspace }}/lambda.zip

除了此前的权限问题,这里还有一个需要注意的问题就是 gem install 或者说 bundle install 的环境。假设我们使用的 gem 是有 C native extesion的,就可能会出现问题——假设我们的 gem 是在 arm64 架构上 compile 而 lambda 是运行在 x86_64 架构上,这显然就会出现无法运行的情况。

在上面的这个 workflow 里,我们是直接使用 ubuntu-latest 环境去编译,如果我们的 lambda 也是运行在 x86_64 架构上,那一般问题不大。但是为了稳妥起见,这种情况下还是采用在 Docker 内部去编译比较好,同时采用 AWS 的 runtime 镜像:

FROM public.ecr.aws/lambda/ruby:3.2

COPY Gemfile Gemfile.lock ${LAMBDA_TASK_ROOT}/

RUN gem install bundler:2.4.20 && \
    bundle config set --local path 'vendor/bundle' && \
    bundle install

COPY lambda_function.rb ${LAMBDA_TASK_ROOT}/    

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "lambda_function.LambdaFunction::Handler.process" ]

Workflow for Serverless

对于使用 serverless 部署的情况,官方有提供他们的 workflow action

Workflow for Jets

直接在本地运行 jets deploy 可能是更便利的方式,类似使用 cap deploy 去部署 Ruby 项目。但这也不妨碍我们使用 Gtihub workflow,下面是一个实例:

name: RSpec and Deploy Lambda

on:
  push:
    branches:
      - main

jobs:
  test:
    name: RSpec
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps: [] # similar above

  deploy:
    name: Deploy to Lambda
    needs: test
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2
      - name: Install Bundler and Dependencies
        run: |
          gem install bundler
          bundle config set --local without 'development test'
          bundle install
      - name: Setup AWS CLI
        uses: aws-actions/setup-aws-cli@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: <your-aws-region>
      - name: Deploy to Lambda
        env:
          JETS_ENV: production
        run: |
          bundle exec jets deploy

总结

在本篇文章中,我们探讨了在 AWS Lambda 上部署 Ruby 代码的各种方法,包括手动和自动方法。下面做一个简单的总结:

  • Lambda Console
    • 易于使用
    • 适合依赖关系少的简答 lambda
  • Zip 和 AWS CLI
    • 需要处理依赖gem并将其与部署包打包
    • 与 Lambda 控制台相比,可对部署过程提供更多控制
  • Serverless Framework
    • 这是一个全面的框架,可简化整个 Lambda 部署流程
    • 需要配置 serverless.yml 文件来定义 AWS 资源和设置
    • Serverless 支持多种编程语言并提供全面广泛的部署
  • Jets Framework
    • AWS Lambda 版本的 Rails 。
    • Ruby亲和性:提供类似 Rails 的结构,学习成本低
    • CloudFormation 集成: 利用 AWS CloudFormation 进行资源管理,允许批量操作资源和版本控制
< Back