无服务器 - 快速指南



无服务器 - 简介

什么是无服务器?

嗯,这个名字给了你不少提示。无需维护服务器即可进行计算——这就是无服务器计算(简称无服务器)的核心。这个概念具有相当的革命性和颠覆性。它已被广泛采用。许多新的应用程序都是从设计无服务器后端开始的,而拥有专用服务器的遗留应用程序也正在缓慢地迁移到无服务器架构。那么是什么导致了无服务器的广泛采用呢?与所有事情一样,经济因素使无服务器非常有利。

你看,使用无服务器,你只为使用的资源付费。假设你每天需要对数据库进行一些例行维护。这个过程每天可能需要大约 10 分钟。

现在,在没有无服务器计算的情况下,你的维护 cron 作业可能驻留在服务器上。除非你在剩余时间内还有其他事情要做,否则你最终可能会为一项需要 10 分钟的任务支付 24 小时的费用。相当浪费钱,对吧?如果有人告诉你有一项新服务,只会为你维护 cron 作业执行的这 10 分钟收费呢?你难道不想简单地切换到这项新服务吗?这就是无服务器采用如此迅速和广泛的原因。它降低了多个组织的后端账单,同时也减少了他们的服务器维护难题。

云服务提供商(AWS、Azure 等)承担了确保无服务器应用程序在需要时且以所需数量可用的难题。因此,在高负载期间,你可能会调用多个无服务器应用程序,而在正常负载期间,你可能会调用单个应用程序。当然,你只会为高负载持续时间内额外调用的时间付费。

再次什么是无服务器?

上面解释的概念似乎很棒,但是你如何实现它呢?你需要一个框架。它被称为,呃,serverless

无服务器框架帮助我们开发和部署设计为以无服务器方式运行的函数/应用程序。该框架更进一步,负责部署无服务器函数运行所需的整个堆栈。什么是堆栈?嗯,堆栈包含你部署、存储和监控无服务器应用程序所需的所有资源。

它包括实际的函数/应用程序、存储容器、监控解决方案等等。例如,在 AWS 的上下文中,你的堆栈将包含你的实际 Lambda 函数、用于函数文件的 S3 存储桶、与你的函数关联的 Cloudwatch 资源等等。无服务器框架为我们创建了这个整个堆栈。这使我们能够完全专注于我们的函数。无服务器消除了维护服务器的难题,而无服务器(框架)消除了创建和部署运行我们的函数所需的堆栈的难题。

无服务器框架还负责为我们的函数/应用程序分配必要的权限。一些应用程序(我们将在本教程中看到示例)甚至需要将数据库链接到它们。无服务器框架再次负责创建和链接数据库。无服务器如何知道在堆栈中包含什么以及要提供哪些权限?所有这些都写在 serverless.yml 文件中,这将是本教程的主要焦点。更多内容将在接下来的章节中介绍。

Explore our latest online courses and learn new skills at your own pace. Enroll and become a certified expert to boost your career.

AWS 中的无服务器

AWS 的许多服务都属于“无服务器计算”的范畴。你可以在这里找到整个组织列表here。有计算服务、集成服务,甚至数据存储服务(是的,AWS 甚至有无服务器数据库)。在本教程中,我们将重点关注 AWS Lambda 函数。那么什么是 AWS Lambda 呢?AWS Lambda 网站将其定义如下:

AWS Lambda 是一种无服务器计算服务,允许你运行代码而无需预置或管理服务器、创建了解工作负载的集群扩展逻辑、维护事件集成或管理运行时。

用通俗的话说,AWS Lambda 是你在 AWS 上进行无服务器计算的窗口。正是 AWS Lambda 使无服务器概念如此流行。你只需定义你的函数和函数的触发器,函数将在你想要调用时被调用,你只需为函数执行所需的时间付费。更重要的是,你可以将 AWS Lambda 与 AWS 提供的几乎所有其他服务链接起来——EC2、S3、dynamoDB 等等。

因此,如果你已经是 AWS 生态系统的一部分,那么 Lambda 集成非常无缝。如果你像我第一次了解 AWS Lambda 时一样不熟悉 AWS 生态系统,它将作为进入 AWS 宇宙的良好网关。

在本教程中,我们将学习使用无服务器框架部署 AWS Lambda 函数的所有知识。你兴奋吗?然后继续下一章开始吧。

无服务器 - 安装

无服务器安装已经在另一个 tutorialspoint 教程中介绍过。在此处进行复制,并进行一些修改和补充。

步骤 1 - 安装 nodejs

首先,你需要安装 nodejs。你可以通过打开命令提示符并键入 **node -v** 来检查你的机器上是否已安装 nodejs。如果已安装,你将获得 node 的版本号。否则,你可以从此处下载并安装 node here.

Installing Node

步骤 2 - 使用 npm 命令安装无服务器

你可以使用以下命令安装无服务器(npm 代表 node 包管理器):

npm install -g serverless

你可以通过运行 **serverless create --help** 来检查它是否成功安装。如果无服务器成功安装,你应该会看到 create 插件的帮助屏幕。

Install Help

请注意,你可以在所有命令中使用简写 **sls** 代替 **serverless**。

步骤 3 - 配置凭据

你需要从 AWS 获取凭据来配置无服务器。为此,你可以在 AWS 控制台中创建一个用户(通过 IAM -> 用户 -> 添加用户),或者点击 IAM -> 用户中的现有用户。如果你正在创建新用户,你需要附加一些必需的策略(例如 Lambda 访问、S3 访问等),或者向用户提供管理员访问权限。

Set Permissions

创建用户后,你将能够看到访问密钥和密钥。请妥善保管这些密钥,并将其保密。

Secret Key

如果你是一个现有用户,你可以按照此处提到的步骤生成新的访问密钥和密钥 here.

一旦你准备好访问密钥和密钥,你就可以使用以下命令在无服务器中配置凭据:

serverless config credentials --provider aws --key 1234 --secret 5678 --profile custom-profile

profile 字段是可选的。如果你留空,默认 profile 为“aws”。记住你设置的 profile 名称,因为你必须在下一个教程中看到的 serverless.yml 文件中提及它。

如果你完成了上述步骤,则无服务器配置已完成。继续下一章以创建你的第一个无服务器项目。

无服务器 - 部署函数

创建新项目

导航到一个新文件夹,你希望在其中创建要部署到无服务器的第一个项目。在该文件夹中,运行以下命令:

sls create --template aws-python3

此命令将创建使用无服务器和 python 运行时部署 lambda 函数的样板代码。

Deploying

请注意,你也可以使用其他运行时。运行 **sls create --help** 以获取所有模板的列表。

创建样板代码后,你将在文件夹中看到两个文件:handler.py 和 serverless.yml。handler.py 是包含 lambda 函数代码的文件。serverless.yml 是告诉 AWS 如何创建 lambda 函数的文件。它是配置文件或设置文件,将成为本教程几章的重点。让我们首先浏览 handler.py 文件。

import json
def hello(event, context):
   body = {
      "message": "Go Serverless v1.0! Your function executed successfully!", "input": event
   }
   response = {
      "statusCode": 200, "body": json.dumps(body)
   }
   return response
   # Use this code if you don't use the http event with the LAMBDA-PROXY
   # integration
   """
   return {
      "message": "Go Serverless v1.0! Your function executed successfully!", "event": event
   }
   """

它包含一个函数 **hello**。此函数接受两个参数:event 和 context。对于任何 AWS Lambda 函数,这两个参数都是必需的。每当调用 lambda 函数时,lambda 运行时都会将两个参数传递给函数——event 和 context。

**event** 参数包含 lambda 函数要处理的数据。例如,如果通过 REST API 触发 lambda 函数,你通过 API 的路径参数或正文发送的任何数据都会作为 event 参数发送到 lambda 函数。更多内容将在后面的章节中介绍。需要注意的是,event 通常是 python **dict** 类型,尽管也可以是 **str**、**float**、**int**、**list** 或 **NoneType** 类型。

**context** 对象是运行时传递给 lambda 函数的另一个参数。它并不经常使用。AWS 官方文档指出,*此对象提供提供有关调用、函数和运行时环境的信息的方法和属性。* 你可以在这里阅读有关 **event** 和 **context** 对象的更多信息 here.

此函数非常简单明了。它仅返回一条包含状态码 200 的消息。底部有一条注释,如果我们不使用带有 LAMBDA-PROXY 设置的 HTTP 事件,则应该使用它。更多内容请参见 API 触发 lambda 章节。

现在,让我们来看一下 serverless.yml 文件。这是一个注释非常多的文件。对于刚接触无服务器的开发者来说,这些注释非常有用。建议您仔细阅读这些注释。在接下来的章节中,我们将学习许多与 serverless.yml 相关的概念。这里我们只浏览一下基本概念。

如果您删除注释后查看 serverless.yml 文件,它将如下所示:

service: aws-serverless
frameworkVersion: '2'

provider:
   name: aws
   runtime: python3.8
   lambdaHashingVersion: 20201221
functions:
   hello:
      handler: handler.hello

service 字段确定您的 lambda 函数和所有所需资源将在其中创建的 CloudFormation 堆栈的名称。您可以将 service 视为您的项目。AWS Lambda 函数执行所需的一切都将在该 service 中创建。您可以设置您选择的 service 名称。

framework version 指的是无服务器框架的版本。这是一个可选字段,通常用于确保与您共享代码的人使用相同的版本号。如果 serverless.yml 中提到的 frameworkVersion 与您机器上安装的 serverless 版本不同,则在部署过程中会收到错误。您还可以为 frameworkVersion 指定一个范围,例如 **frameworkVersion − >=2.1.0 && <3.0.0**。您可以在 此处 阅读更多关于 frameworkVersion 的信息。

下一个部分 **provider** 可以被认为是一组全局设置。我们将在后面的章节中讨论 provider 下包含的其他参数。在这里,我们将关注可用的参数。**name** 字段确定您的平台环境的名称,在本例中为 aws。runtime 为 python3.8,因为我们使用了 python3 模板。lambdaHashingVersion 指的是框架应使用的哈希算法的名称。

请注意,如果您在上章的配置凭证步骤中添加了自定义配置文件,则需要在 provider 中添加 profile 参数。例如,我将我的配置文件名称设置为 yash-sanghvi。因此,我的 provider 看起来像这样:

provider:
   name: aws
   runtime: python3.8
   lambdaHashingVersion: 20201221
   profile: yash-sanghvi

最后,functions 块定义所有 lambda 函数。这里我们只有一个函数,在 handler 文件中。函数的名称为 hello。函数的路径在 handler 字段中提到。

部署函数

要部署函数,您需要打开命令提示符,导航到包含 serverless.yml 文件的文件夹,然后输入以下命令:

sls deploy -v

**-v** 是一个可选参数,表示详细输出。它可以帮助您更好地理解后台进程。函数部署后,您应该能够在 us-east-1 区域(默认区域)的 AWS 控制台中看到它。您可以使用“测试”功能从控制台调用它(您可以保留相同的默认事件,因为我们的 lambda 函数无论如何都不会使用事件输入)。您也可以使用命令提示符进行测试:

sls invoke --function hello

请注意,如果您的函数与其他 AWS 服务(如 S3 或 dynamoDB)交互,则您不能始终在本地测试您的函数。只有最基本的函数可以在本地测试。

从现有项目部署函数

如果您想将现有项目部署到 AWS,请修改现有函数,使其仅接收 **event** 和 **context** 作为参数。接下来,在文件夹中添加一个 serverless.yml 文件,并在 serverless.yml 中定义您的函数。然后打开命令提示符,导航到该文件夹,然后输入 **sls deploy -v**。这样,您的现有函数也可以部署到 AWS Lambda。

Serverless - 区域、内存大小、超时

在上章中,我们学习了如何使用 serverless 部署我们的第一个函数。本章,我们将学习一些可以对函数进行的配置。我们将主要关注区域、内存大小和超时。

区域

默认情况下,使用 serverless 部署的所有 lambda 函数都将在 us-east-1 区域中创建。如果您希望您的 lambda 函数在其他区域创建,您可以在 provider 中指定。

provider:
   name: aws
   runtime: python3.6
   region: us-east-2
   profile: yash-sanghvi

在一个 serverless.yml 文件中,无法为不同的函数指定不同的区域。您应该在一个 serverless.yml 文件中只包含属于单个区域的函数。属于不同区域的函数可以使用单独的 serverless.yml 文件部署。

内存大小

AWS Lambda 会根据选择的内存大小按比例分配 CPU。根据最近发布的变更,您可以为您的 lambda 函数选择高达 10GB 的 RAM(之前约为 3GB)。

选择的 RAM 越高,分配的 CPU 越高,函数执行速度越快,执行时间越短。AWS Lambda 会按消耗的 GB-s 收费。因此,如果一个 1 GB RAM 的函数需要 10 秒才能执行,而一个 2 GB RAM 的函数需要 5 秒才能执行,那么这两个调用的收费金额相同。将内存加倍后时间是否减半很大程度上取决于函数的性质,您可能会或可能不会从增加内存中获益。关键在于分配的内存大小是每个 lambda 函数的重要设置,也是您希望控制的一个设置。

使用 serverless,很容易为 serverless.yml 文件中定义的函数设置内存大小的默认值。也可以为不同的函数定义不同的内存大小。让我们看看如何操作。

为所有函数设置默认内存大小

默认值始终在 provider 中提到。此值将由 serverless.yml 中的所有函数继承。**memorySize** 键用于设置此值。该值以 MB 为单位表示。

provider:
   name: aws
   runtime: python3.6
   region: us-east-2
   profile: yash-sanghvi
   memorySize: 512 #will be inherited by all functions

如果您没有在 provider 中或单个函数中指定 memorySize,则将考虑默认值 1024。

为某些函数设置自定义内存大小

如果您希望某些函数的值与默认内存不同,则可以在 serverless.yml 的 functions 部分中指定。

functions:
   custom_memory_func: #will override the default memorySize
      handler: handler.custom_memory
      memorySize: 2048
  default_memory_func: #will inherit the default memorySize from provider
      handler: handler.default_memory

超时

与 memorySize 一样,超时(以秒为单位)的默认值可以在 provider 中设置,并且可以在 functions 部分中为单个函数指定自定义超时。

如果您没有指定全局或自定义超时,则默认值为 6 秒。

provider:
   name: aws
   runtime: python3.6
   region: us-east-2
   profile: yash-sanghvi
   memorySize: 512 #will be inherited by all functions
   timeout: 50 #will be inherited by all functions
  
functions:
   custom_timeout: #will override the default timeout
      handler: handler.custom_memory
      timeout: 30
  default_timeout_func: #will inherit the default timeout from provider
      handler: handler.default_memory

确保将超时设置为保守值。它不应该太小以至于您的函数频繁超时,也不应该太大以至于函数中的错误导致您支付过高的账单。

无服务器 - 服务

您不会希望为部署的每个函数创建一个单独的 serverless.yml 文件。那将非常繁琐。幸运的是,serverless 提供了在同一个 serverless.yml 文件中部署多个函数的机制。所有这些函数都属于一个名为“service”的组。service 名称通常是 serverless.yml 文件中定义的第一个内容。

service: my-first-service

provider:
   name: aws
   runtime: python3.6
   stage: prod
   region: us-east-2
   profile: yash-sanghvi
  
   functions:
      func1:
      handler: handler1.func1

   func2:
      handler: handler2.func2

部署时,同一个 service 中的所有函数在 AWS Lambda 控制台中采用以下名称格式:**service_name-stage_name-function_name**。因此,上面的示例中的两个函数部署后将采用名称:**my-first-service-prod-func1** 和 **my-first-service-prod-func2**。stage 参数帮助您区分代码开发的不同阶段。

因此,如果您的函数处于开发阶段,您可以使用阶段 **dev**;如果它处于测试阶段,您可以使用阶段 **test**;如果它处于生产阶段,您可以使用阶段 **prod**。这样,您可以确保对 dev 阶段所做的更改不会影响生产代码。阶段名称并非一成不变。**dev、test、prod** 只是示例。

您可以选择任何您想要的阶段名称。请注意,如果您有 API Gateway 触发的 lambda(更多内容将在后面的章节中介绍),那么每个阶段的端点将有所不同。

此外,如果您转到 AWS Lambda 控制台较少使用的“应用程序”部分,您将能够看到带有阶段的整个服务。

AWS Lambda

如果您单击您选择的 service 和 stage 组合,您将能够在一个地方看到服务使用 的所有资源:Lambda 函数、API 网关、事件规则、日志组、S3 存储桶等等。

Hello World Python Dev

更有趣的是,您可以转到“监控”选项卡,查看整个服务的性能:调用次数、平均持续时间、错误计数等。您可以了解哪个函数对您的账单贡献最大。当您的服务中有多个函数时,监控每个函数的性能变得非常困难。服务级别的“监控”选项卡在这里非常有帮助。

Deployments

最后,“部署”选项卡可以帮助您查看服务的过去所有部署以及部署的状态。

Monitoring

无服务器 - 定时 Lambda 函数

通常,您需要您的函数以固定的时间间隔调用。它可以是一天一次,一周两次,工作日每分钟一次,等等。Serverless 提供两种类型的事件,以便以固定的频率调用函数。它们是 cron 事件和 rate 事件。

Cron 事件

cron 事件比 rate 事件具有更大的灵活性。唯一的缺点是它不如 rate 事件容易理解。cron 表达式的语法在 AWS 文档 中定义:

cron(minutes hours day-of-month month day-of-week year)

如您所见,cron 表达式由 6 个字段组成。每个字段都可以接受一些值,以及一些 AWS 所称的 *通配符*。

让我们先来看看可接受的值:

  • **分钟** - 0-59

  • **小时** - 0-23

  • **月份中的某一天** - 1-31

  • **月份** - 1-12 或 JAN-DEC

  • **星期中的某一天** - 1-7 或 SUN-SAT

  • **年份** - 1970-2199

现在可接受的值已经清楚了,让我们来看看通配符。cron 表达式中总共有 8 个可能的通配符(一些允许用于所有 6 个字段,一些仅允许用于特定字段)。这里列出它们:

  • **\*(星号,允许用于所有 6 个字段)** - 这是最流行的通配符。它只是说包含字段的所有值。小时字段中的 * 表示 cron 将每小时运行一次。月份中的某一天字段中的 * 表示 cron 将每天运行一次。

  • , (逗号,所有6个字段都允许) − 用于指定多个值。例如,如果希望您的 cron 在每小时的第 5、7 和 9 分钟运行,则分钟字段应如下所示:5,7,9。类似地,在星期字段中使用 MON,TUE,WED,THU,FRI 可以表示 cron 仅在工作日运行。

  • - (短横线,所有6个字段都允许) − 此通配符指定范围。在之前的通配符示例中,为了指定工作日,无需指定 5 个逗号分隔的值,只需编写 MON-FRI 即可。

  • ? (问号,仅限于月份和星期) − 这就像一个“不关心”通配符。如果在星期字段中指定了 MON,则不关心星期一落在哪一天。因此,您将在月份字段中输入 ?。类似地,如果希望 cron 在每月的第 5 天运行,则在月份字段中输入 5,在星期字段中输入 ?,因为您不关心每月的第 5 天是星期几。请注意,AWS 文档明确指出,不能同时在星期字段和月份字段中使用 *。如果在一个字段中使用 *,则必须在另一个字段中使用 ?

  • / (正斜杠,除月份外,所有 5 个字段都允许) − 此字段指定增量。如果在小时字段中输入 0/2,则此 cron 将每隔偶数小时运行一次(0、0+2、0+2+2 等)。如果在小时字段中指定 1/2,则此 cron 将每隔奇数小时运行一次(1、1+2、1+2+2 等)。正如您可能猜到的那样,/ 前面的值是起始值,/ 后面的值定义增量。

  • L (仅限于月份和星期) − 指定月份的最后一天或星期的最后一天。

  • W (仅限于月份) − 指定最接近该特定月份的某一天的工作日(星期一到星期五)。因此,如果在月份字段中指定 8W,并且它对应于一个工作日,例如星期二,则 cron 将在第 8 天触发。但是,如果 8 对应于周末,例如星期六,则 cron 将在第 7 天(星期五)触发。如果第 8 天是星期日,则 cron 将在第 9 天(星期一)触发。这是最少使用的通配符之一。

  • # (仅限于星期) − 这是一个非常特殊的通配符,最好通过示例来理解。假设您希望 cron 在母亲节运行。母亲节每年都在五月的第二个星期日。因此,您的月份字段将包含 MAY 或 5。但是,如何指定第二个星期日呢?这里就用到了井号。表达式为 0#2。通配符前面的值是星期几(0 代表星期日,1 代表星期一,以此类推)。通配符后面的值指定出现次数。因此,这里的 2 指的是星期日的第二次出现,即第二个星期日。

现在,要为您的 lambda 函数定义一个 cron 触发器,您只需在 serverless.yml 文件中函数的 events 密钥内指定 cron 表达式即可。

functions:
   cron_triggered_lambda:
      handler: handler.hello
      events:
         - schedule: cron(10 03 * * ? *) #run at 03:10 (UTC) every day. 

一些示例

下面是一些 cron 表达式的示例 −

  • cron(30 15 ? * MON-FRI *) − 在每个工作日的 15:30 (UTC) 触发。

  • cron(0 9 ? 6 0#3 *) − 在 6 月份的第三个星期日 (父亲节) 的 09:00 (UTC) 触发。

  • cron(0/15 * ? * MON *) − 每 15 分钟在星期一触发一次。

  • cron(0/30 9-18 ? * MON-FRI *) − 在工作日(对应于许多地方的办公时间)的上午 9 点到下午 5:30 之间每 30 分钟触发一次。

速率事件

与 cron 表达式相比,这要简单得多。语法很简单:rate(value unit)。例如,rate(5 minutes)。

值可以是任何正整数,允许的单位是分钟、小时、天。

为您的 lambda 函数定义速率触发器与定义 cron 触发器类似。

functions:
   rate_triggered_lambda:
      handler: handler.hello
      events:
         - schedule: rate(10 minutes) #run every 10 minutes

一些示例

  • rate(2 hours) − 每 2 小时触发一次。

  • rate(1 day) − 每天触发一次(UTC 00:00)。

  • rate(90 minutes) − 每 90 分钟触发一次。

您可能已经意识到,速率表达式的简单性是以降低灵活性为代价的。您可以将速率用于每 N 分钟/小时/天运行的 lambda。要执行更复杂的操作,例如仅在工作日触发您的 lambda,您必须使用 cron 表达式。

请注意,如果您的 cron 表达式导致触发时间小于一分钟,则不支持。

参考资料

无服务器 - API 网关触发的 Lambda 函数

API 网关是触发 lambda 的另一种常用方法,就像 cron/rate 事件一样。基本上,您可以为您的 lambda 函数获取一个 URL 终结点。此 URL 属于连接到您的 lambda 的 API 网关。每当您在浏览器或应用程序中调用 URL 时,您的 lambda 函数就会被调用。在本章中,我们将了解如何使用无服务器框架将 API 网关连接到您的 lambda 函数,以及如何测试它。

HTTP 事件

要将 API 网关链接到 lambda 函数,我们需要在 serverless.yml 中的函数定义中创建 HTTP 事件。以下示例显示了如何将您的 lambda 函数链接到 REST API 并使用 GET 请求触发它。

functions:
   user_details_api:
      handler: handler.send_user_details
      events:
         - http:
            path: details/{user_id}
            method: get
            integration: lambda-proxy
            cors: true
		  
   location_api:
      handler: handler.send_location
      events:
         - http:
            path: location/{user_id}
            method: get
            integration: lambda-proxy
            cors: true

让我们逐一分解这些密钥。我们只讨论上面列表中的第一个函数 (user_details_api)。下面介绍的概念也适用于其他函数。

path 的值指定调用 URL 后面的地址。上述示例中定义的两个函数将共享相同的终结点,但一个将使用终结点/details/{user_id} 调用,而另一个将使用终结点/location/{user_id} 调用。花括号中的元素是路径参数。我可以替换 user_id 中的任何值,并且可以对 lambda 函数进行编程以返回该特定用户的详细信息(请参见下面的示例函数)。

method 的值指示请求方法。常用的方法是 get 和 post。还有其他几种方法。深入研究这些方法的细节超出了本章的范围。还有一个tutorialspoint 上的文章,您可以参考以了解详细信息。

integration 字段指定 lambda 函数如何与 API 网关集成。默认值为 lambda-proxy,其他可能的选项为 lambda、http、http-proxy、mock。这两个选项中最常用的选项是 lambda 和 lambda-proxy。简单来说,lambda-proxy 将完全控制权交给您的 lambda 函数,而 lambda 将部分控制权交给 API 网关,部分控制权交给 lambda 函数。

如果您选择 lambda-proxy 作为集成类型,则整个 HTTP 请求将以原始形式传递给您的 lambda 函数,并且 lambda 函数发送的响应将无需更改地传递给发出请求的客户端。因此,您必须在 lambda 函数的响应中定义 statusCode 和 headers。

如果您选择 lambda 作为集成类型,则您的 API 网关可以在将其传递给 lambda 函数之前修改接收到的请求。类似地,它还可以修改 lambda 函数发送的响应,然后再将其转发给客户端。API 网关将状态码和标头添加到响应中,因此 lambda 函数只需担心发送正文即可。两种选项各有优缺点。

如果您喜欢简单性,可以使用 lambda-proxy。如果您不介意一些复杂性(因为您需要同时处理 lambda 函数的代码和 API 网关的配置),但需要更多控制,可以选择 lambda

您可以阅读更多关于这两种类型之间区别的信息这里。在其他集成类型中,httphttp-proxy 用于将 API 网关与 HTTP 后端而不是 lambda 函数集成,因此与我们无关。mock 用于在不调用后端的情况下测试 API。

corstrue 配置启用 CORS(跨源资源共享)。简单来说,这意味着您允许来自另一个域的服务器的请求。没有 corstrue,只允许来自同一域的请求。当然,您也可以只允许某些特定域,而不是允许所有域。要了解如何执行此操作,请参阅文档

在无服务器中,对于 API 网关触发的 lambda,还可以进行更多配置。强烈建议您阅读文档,或者至少将链接添加为书签,以便在需要时查找。

示例 Lambda 函数

此时,您可能想知道您创建了 API 网关触发的函数,但是如何在 lambda 函数中访问路径参数?以下 python 中的示例 lambda 函数将解答此问题。当集成类型为 lambda-proxy 时,我们基本上使用 'pathParameters' 属性。

import json

def lambda_handler(event, context):
   # TODO implement
   # print(event) 
   #helps you see the entire input request. The printed output can be found in CloudWatch logs
      user = event['pathParameters']['user_id']
      return {
         'statusCode': 200,
         'body': json.dumps('Hello ' + str(user))
      }

访问终结点

现在,您可能还有另一个问题,即如何访问终结点。有多种方法可以做到这一点。第一种方法是通过无服务器部署。每当您通过服务部署一个函数或多个函数时,终结点都会显示在无服务器部署的末尾。

第二种方法是通过 Lambda 控制台。如果您在 lambda 控制台中导航到您的函数,则可以看到附加到它的 API 网关。单击它应该会显示终结点。

API Gateway

请注意,如上所述,一个服务中的所有函数共享相同的终结点。path 属性区分一个函数的实际触发 URL 与另一个函数的实际触发 URL。

参考资料

无服务器 - 包含/排除

我们在“部署函数”一章中已经看到,要将现有项目中的函数部署到 AWS Lambda,需要修改函数以接受**event** 和 **context** 作为参数,并且需要在项目文件夹中添加一个 serverless.yml 文件,其中定义了这些函数。然后运行**serverless deploy** 即可完成部署。

尤其是在将大型现有项目中的某些函数迁移到 AWS Lambda 时,经常会遇到大小方面的挑战。如果项目足够大,很可能会超过 AWS 对 Lambda 函数施加的大小限制 (250 MB,包括应用程序代码及其依赖项)。

一些依赖项,例如 NumPy,本身就占用大量空间。例如,NumPy 大约 80 MB,SciPy 也差不多,等等。在这种情况下,应用程序代码的剩余空间非常有限,需要一种方法来排除 Lambda 部署包中不需要的文件。幸运的是,serverless 使这变得非常容易。

include 和 exclude 字段

正如你所猜想的那样,可以使用“exclude”标签指定要从部署构建中排除的文件和文件夹。默认情况下,未在 exclude 部分中指定的所有文件/文件夹都将包含在内。“include”标签有什么用呢?如果你想全局排除某个文件夹,但又想包含该文件夹内的一些文件或子文件夹,则可以在“include”标签中指定这些文件/子文件夹。这样,该文件夹内的所有其他文件都将被排除,只有在“include”部分中指定的文件才会保留。下面的示例将更好地解释这一点。

service: influx-archive-pipeline

provider:
   name: aws
   runtime: python3.6
   stage: prod
   region: us-east-2
   profile: yash-sanghvi
   timeout: 900
   memorySize: 1024
  
# you can add packaging information here
package:
   include:
      - src/models/config.py
      - src/models/lambda_apis/**
      - src/models/scheduled_lambdas/**

   exclude:
      - docs/**
      - models/**
      - notebooks/**
      - references/**
      - reports/**
      - src/data/**
      - src/visualization/**
      - src/models/**
      - utils/**
	
functions:
   user_details_api:
      handler: src/models/lambda_apis/user_details_api.sync_user_details
      events:
         - http:
            path: details/{user_id}
            method: get
            integration: lambda
            cors: true

   monitoring_lambda:
      handler: src/models/scheduled_lambdas/monitoring_lambda.periodic_monitoring
      events:
         - schedule: cron(15 16 * * ? *)

从上面的 serverless.yml 文件可以看出,我们排除了包含 serverless.yml 的根文件夹中的大部分文件夹。我们甚至排除了 src/models 文件夹。但是,我们想包含 src/models 中的 2 个子文件夹和 1 个文件。因此,这些文件已在“include”部分中特别添加。请注意,默认情况下,任何不是 exclude 部分一部分的文件/文件夹都将包含在内。

请注意两个 Lambda 函数的路径。它们都位于 src/models 中。虽然 src/models 默认情况下会被排除,但这些函数特别位于“include”部分中提到的子文件夹中。因此,它们将能够正常执行。如果我添加一个位于 src/data 中的函数,则该函数将不被允许,因为 src/data 的所有内容都被排除了。

请注意,指定**/** 表示该文件夹内的所有内容(文件/子文件夹)都将被包含。因此,如果 docs 文件夹包含 10 个子文件夹和 12 个文件,并且所有这些都需要排除,则**-docs/** 就可以完成任务。我们不需要单独提及每个文件/文件夹。

无服务器 - 插件

随着 serverless 的普及,对更多针对特定用户案例的功能的需求自然会增加。这些需求可以通过插件来满足。顾名思义,插件是可选的,你只需要安装你需要的插件即可。在本节中,我们将了解如何访问 serverless 可用的多个插件,如何安装这些插件以及如何在 serverless.yml 中引用它们。

浏览插件列表

所有可用于 serverless 的插件都可以在www.serverless.com/plugins/ 找到。

你可以在此处搜索插件。例如,如果你搜索“Python”,你将看到专门为 Python 运行时开发的几个插件。它们按照流行程度的顺序排列。

Plugins

让我们看一下最流行的 Python 插件(撰写本文时):Python Requirements。单击该插件。这将打开与该插件相关的详细文档。

Serverless Python Requirements

此文档涵盖了两个最重要的方面:安装插件以及在 serverless.yml 中引用它。这适用于任何插件。你只需要打开其文档即可了解该插件的安装和使用方法。回到 Python Requirements 插件,文档指出此插件会自动捆绑来自 requirements.txt 的依赖项,并在你的 PYTHONPATH 中提供它们。

换句话说,如果你的 Lambda 函数需要额外的依赖项,例如 pandas、numpy、matplotlib 等,你只需要在一个 requirements.txt 文件中指定这些依赖项,该文件与你的 serverless.yml 文件位于同一文件夹中。然后,此插件将完成其余工作。你甚至可以在 requirements.txt 中指定库的版本号。例如,这是一个示例 requirements.txt 文件:

aws-psycopg2==1.2.1
boto
boto3==1.7.62
botocore==1.10.62
numpy==1.14.5
pandas==0.25.0
scipy==1.5.2
sqlalchemy==1.2.15

如你所见,你可以只提及依赖项名称,也可以添加版本号(用 == 符号分隔)。当然,依赖项以及应用程序代码的大小不应超过 250 MB。因此,务必仅包含实际需要的依赖项。

现在,让我们回到我们的插件。我们已经准备好 requirements.txt 文件。下一步是安装插件。打开你的命令提示符并导航到包含 serverless.yml 文件的项目文件夹。然后,按照文档说明,运行以下命令来安装插件:

sls plugin install -n serverless-python-requirements

事实上,如果你将**serverless-python-requirements**替换为任何其他插件名称,上述命令对于大多数插件仍然有效。但是,建议你在安装新插件时,按照文档中给出的安装命令操作。运行上述命令时,你应该会看到类似于下图中的消息:

SLS Plugin Install

如你所见,在项目文件夹中创建了一个 packages.json 文件。如果你的项目文件夹中已存在 packages.json 文件,它将被编辑以包含上述插件。此外,serverless.yml 文件将自动编辑以包含已安装的插件。如果你现在打开 serverless.yml 文件,你应该会看到添加了以下几行:

plugins:
   - serverless-python-requirements

这意味着在 serverless.yml 中对插件的引用是自动完成的。此插件有几个相关的设置,可以在文档中找到。我们将在下一章讨论与“交叉编译”相关的设置。但现在,让我们只看看使用此插件的效果。我在我的 requirements.txt 中添加了 numpy。我的 handler.py 文件如下所示:

import time
import numpy

def hello(event, context):
   print("second update!")
   time.sleep(4)
   print(numpy.random.randint(100))
   return("hello")

现在,让我们将其部署到 Lambda。你应该会看到类似于下图中的消息。关注包的大小。它现在 > 14 MB(这是压缩包的大小),而不是添加插件之前的 ~10 kB,因为 numpy 依赖项也一起打包了。

Deploy

这证明依赖项现在与应用程序代码一起打包了。你可以使用**sls invoke local -f function_name**在本地对其进行测试。如果你使用的是 Windows 或 Mac,则很有可能在 AWS Lambda 控制台中测试已部署的 Lambda 函数会抛出错误,类似于以下错误:

Unable to import module 'handler': 

IMPORTANT: PLEASE READ THIS FOR ADVICE ON HOW TO SOLVE THIS ISSUE!

Importing the numpy C-extensions failed. This error can happen for
many reasons, often due to issues with your setup or how NumPy was
installed.

We have compiled some common reasons and troubleshooting tips at:

   https://numpy.com.cn/devdocs/user/troubleshooting-importerror.html

Please note and check the following:

  * The Python version is: Python3.8 from "/var/lang/bin/python3.8"
  * The NumPy version is: "1.19.4"

and make sure that they are the versions you expect.
Please carefully study the documentation linked above for further help.

Original error was: No module named 'numpy.core._multiarray_umath'

请继续阅读下一章,了解为什么会出现此错误以及如何处理它。

无服务器 - 打包依赖项

在上一章中,我们了解了如何在 serverless 中使用插件。我们特别关注了 Python Requirements 插件,并了解了如何使用它将 numpy、scipy、pandas 等依赖项与你的 Lambda 函数的应用程序代码捆绑在一起。我们甚至看到了部署需要 numpy 依赖项的函数的示例。我们看到它在本地运行良好,但在 AWS Lambda 控制台中,如果你使用的是 Windows 或 Mac 机器,则会遇到错误。让我们了解为什么函数可以在本地运行,但在部署后却无法运行。

如果你查看错误消息,你会得到一些提示。我特别指的是一行:“导入 numpy C 扩展失败”。现在,许多重要的 python 包,例如 numpy、pandas、scipy 等,都需要编译 C 扩展。如果我们在 Windows 或 Mac 机器上编译它们,那么 Lambda(Linux 环境)在尝试加载它们时会抛出错误。所以重要的问题是,有什么方法可以避免此错误。Docker 登場了!

什么是 Docker?

根据维基百科,Docker 是一套平台即服务 (PaaS) 产品,它们使用操作系统级虚拟化来将软件打包到称为容器的包中。如果你再多浏览一下 Docker 的维基百科页面,你还会看到一些更相关的语句:Docker 可以将应用程序及其依赖项打包到可在任何 Linux、Windows 或 macOS 计算机上运行的虚拟容器中。这使应用程序能够在各种位置运行,例如本地、公共云和/或私有云。我认为在上述语句之后应该非常清楚了。我们出现错误是因为在 Windows/Mac 上编译的 C 扩展在 Linux 中不起作用。

我们可以通过将应用程序打包到可在任何操作系统上运行的容器中来简单地绕过该错误。Docker 在后台为实现这种操作系统级虚拟化所做的工作超出了本章的范围。

安装 Docker

你可以访问https://docs.docker.net.cn/engine/install/ 来安装 Docker Desktop。如果你使用的是 Windows 10 家庭版,则 Windows 版本应至少为 1903(2019 年 5 月更新)。因此,你可能需要升级你的 Windows 10 操作系统才能安装 Docker Desktop。Windows 专业版或企业版没有此类限制。

在 serverless 中使用 dockerizePip

在你的机器上安装 Docker Desktop 后,你只需要在你的 serverless.yml 文件中添加以下内容即可使用 docker 打包你的应用程序和依赖项:

custom:
   pythonRequirements:
      dockerizePip: true

请注意,如果你从上一章开始一直跟着我操作,你很可能已经将代码部署到 Lambda 一次了。这会在你的本地存储中创建静态缓存。默认情况下,serverless 将使用该缓存来捆绑依赖项,因此不会创建 docker 容器。因此,为了强制 serverless 使用 docker,我们将向 pythonRequirements 添加另一个语句:

custom:
   pythonRequirements:
      dockerizePip: true
	useStaticCache: false #not necessary if you will be deploying the code to lambda for the first time.

如果你第一次部署到 Lambda,则不需要此最后一条语句。通常,你应该将 useStaticCache 设置为 true,因为这会在你没有更改依赖项或打包方式时节省一些打包时间。

添加这些内容后,serverless.yml 文件现在如下所示:

service: hello-world-python
provider:
   name: aws
   runtime: python3.6
   profile: yash-sanghvi
   region: ap-south-1

functions:
   hello_world:
      handler: handler.hello
      timeout: 6
      memorySize: 128
	
plugins:
   - serverless-python-requirements
  
custom:
   pythonRequirements:
      dockerizePip: true
	useStaticCache: false #not necessary if you will be deploying the code to lambda for the first time.
  

现在,当你运行**sls deploy -v**命令时,请确保 docker 在后台运行。在 Windows 上,你只需在“开始”菜单中搜索 Docker Desktop 并双击该应用程序即可。你很快就会收到一条消息,表明它正在运行。你也可以通过 Windows 电池图标附近的弹出窗口来验证这一点。如果你在那里看到 docker 图标,则表示它正在运行。

Docker

现在,当你在 AWS Lambda 控制台中运行你的函数时,它应该可以正常工作了。恭喜!

但是,在 AWS Lambda 控制台的“函数代码”部分,您会看到一条消息,提示“Lambda 函数“hello-world-python-dev-hello_world”的部署包太大,无法启用内联代码编辑。但是,您仍然可以调用您的函数。”

Function Code

看来添加 Numpy 依赖项使包大小过大,因此我们甚至无法在 lambda 控制台中编辑应用程序代码。我们如何解决这个问题?请继续阅读下一章以了解解决方案。

参考资料

无服务器 - 层创建

什么是层?

层是一种隔离代码块的方法。假设您想在应用程序中导入 NumPy 库。您信任该库,并且几乎没有机会更改该库的源代码。因此,如果您不希望 NumPy 的源代码弄乱您的应用程序工作区,您会怎么做呢?简单来说,您只需要 NumPy 位于其他地方,与您的应用程序代码隔离即可。层允许您做到这一点。您可以简单地将所有依赖项(NumPy、Pandas、SciPy 等)捆绑在一个单独的层中,然后在 serverless 中的 lambda 函数中引用该层。就是这样!现在可以将该层中捆绑的所有库导入到您的应用程序中。同时,您的应用程序工作区保持完全整洁。您只需查看应用程序代码即可进行编辑。

Layer ARN

照片由 Iva RajovicUnsplash 上提供,表示层中的代码分离

层真正酷的一点是它们可以在函数之间共享。假设您部署了一个带有包含 NumPy 和 Pandas 的 python-requirements 层的 lambda 函数。现在,如果另一个 lambda 函数需要 NumPy,则无需为此函数部署单独的层。您可以简单地使用先前函数的层,它也可以与新函数一起正常工作。

这将节省您在部署期间的大量宝贵时间。毕竟,您只需要部署应用程序代码。依赖项已存在于现有层中。因此,许多开发人员将依赖项层保存在单独的堆栈中。然后,他们在所有其他应用程序中使用此层。这样,他们就不需要反复部署依赖项。毕竟,依赖项相当庞大。仅 NumPy 库就大约有 80 MB 大。每次更改应用程序代码(可能只有几 KB)时都部署依赖项将非常不方便。

添加依赖项层只是一个示例。还有其他几个用例。例如,serverless.com 上提供的示例涉及使用 FFmpeg 工具创建 GIF。在该示例中,他们将 FFmpeg 工具存储在一个层中。总而言之,AWS Lambda 允许我们为每个函数最多添加 5 个层。唯一的条件是 5 个层的总大小和应用程序应小于 250 MB。

创建 python-requirements 层

现在让我们看看如何使用 serverless 创建和部署包含所有依赖项的层。为此,我们需要 **serverless-python-requirements** 插件。此插件仅适用于 Serverless 1.34 及更高版本。因此,如果您使用的是低于 1.34 的版本,则可能需要升级您的 Serverless 版本。您可以使用以下命令安装插件:

sls plugin install -n serverless-python-requirements

接下来,您在 serverless.yml 的 plugins 部分添加此插件,并在 custom 部分提及其配置:

plugins:
   - serverless-python-requirements
custom:
   pythonRequirements:
      dockerizePip: true
      layer: true

这里,**dockerizePip** - **true** 启用 docker 的使用,并允许您在 docker 容器中打包所有依赖项。我们在上一章讨论了使用 docker 打包的方法。**layer** - **true** 告诉 serverless 应将 python 依赖项存储在单独的层中。现在,您可能想知道 serverless 如何知道要打包哪些依赖项?答案,如插件章节中所述,在于 requirements.txt 文件。

定义层插件和自定义配置后,您可以按如下方式将层添加到 serverless 中的各个函数:

functions:
   hello:
      handler: handler.hello
      layers:
         - { Ref: PythonRequirementsLambdaLayer }

关键字 **PythonRequirementsLambdaLayer** 来自 CloudFormation 参考。一般来说,它源自层的名称。语法是“LayerNameLambdaLayer”(标题大小写,无空格)。在我们的例子中,由于层名称是 python requirements,因此引用变为 PythonRequirementsLambdaLayer。如果您不确定 lambda 层的名称,您可以按照以下步骤获取:

  • 运行 **sls package**

  • 打开 .serverless/cloudformation-template-update-stack.json

  • 搜索“LambdaLayer”

使用同一区域中另一个函数的现有层

正如我在开头提到的那样,层真正酷的一点是能够在函数中使用现有层。这可以通过使用现有层的 ARN 来轻松完成。使用 ARN 将现有层添加到函数的语法非常简单:

functions:
   hello:
      handler: handler.hello
      layers:
         - arn:aws:lambda:region:XXXXXX:layer:LayerName:Y

就是这样。现在,具有指定 ARN 的层将与您的函数一起工作。如果层包含 NumPy 库,您可以直接在您的“hello”函数中调用 **import numpy**。它将运行而不会出现任何错误。

如果您想知道从哪里可以获取 ARN,实际上非常简单。只需导航到 AWS 控制台中包含该层的函数,然后单击“层”。

Layer ARN

当然,如果该层不属于您的帐户,则需要将其公开共享或专门与您的帐户共享。稍后将详细介绍。

另外,请记住,该层应与您的应用程序兼容。不要指望与 node.js 运行时兼容的层能够与在 python3.6 运行时中创建的函数一起运行。

非依赖项/通用层

如开头所述,层的主要功能是隔离代码块。因此,它们不需要只包含依赖项。它们可以包含您指定的任何代码片段。在 **custom** 中的 **pythonRequirements** 中调用 **layer: true** 是 **serverless-python-requirements** 插件实现的一种快捷方式。但是,要创建通用层,serverless.yml 中的语法,如 serverless 文档 中所述,如下所示:

layers:
   hello:
      path: layer-dir # required, path to layer contents on disk
      name: ${opt:stage, self:provider.stage, 'dev'}-layerName # optional, Deployed Lambda layer name
      description: Description of what the lambda layer does # optional, Description to publish to AWS
      compatibleRuntimes: # optional, a list of runtimes this layer is compatible with
         - python3.8
      licenseInfo: GPLv3 # optional, a string specifying license information
      # allowedAccounts: # optional, a list of AWS account IDs allowed to access this layer.
      #   - '*'
      # note: uncommenting this will give all AWS users access to this layer unconditionally.
      retain: false # optional, false by default. If true, layer versions are not deleted as new ones are created

由于提供了注释,因此各种配置参数是不言自明的。除了“path”之外,所有其他属性都是可选的。path 属性是您希望从应用程序代码中隔离的选择目录的路径。它将被压缩并发布为您的层。例如,在 serverless 上的示例项目 中,他们将 FFmpeg 工具托管在一个层中,他们在名为“layer”的单独文件夹中下载该工具,并在 path 属性中指定该文件夹。

layers:
   ffmpeg:
      path: layer

如前所述,我们可以在 **layers** 属性中最多添加 5 个层。

要使用这些通用层中的函数,您可以再次使用 CloudFormation 引用或指定 ARN。

允许其他帐户访问层

只需在“allowedAccounts”属性中提及帐户编号,即可向更多帐户提供对您的层的访问权限。例如:

layers:
   testLayer:
      path: testLayer
      allowedAccounts:
         - 999999999999 # a specific account ID
         - 000123456789 # a different specific account ID

如果要使层公开访问,可以在 allowedAccounts 中添加“*”:

layers:
   testLayer:
      path: testLayer
      allowedAccounts:
      - '*'

参考资料

无服务器 - 使用 DynamoDB 的 REST API

到目前为止,我们已经学习了与无服务器 lambda 部署相关的几个概念。现在是时候查看一些示例了。在本章中,我们将研究 Serverless 官方提供的示例之一。我们将创建一个 REST API(正如其名称所示)。正如您可能猜到的那样,我们的所有 lambda 函数都将由 API Gateway 触发。我们的 lambda 函数将与 dynamoDB 表(本质上是一个待办事项列表)交互,用户将能够使用将要公开的端点执行多项操作,例如创建新项目、获取现有项目、删除项目等部署后。如果您不熟悉 REST API,您可以在这里了解更多信息。

代码演练

代码可在 GitHub 上找到:https://github.com/serverless/examples/tree/master/aws-python-rest-api-with-dynamodb

我们将查看项目结构,讨论一些我们之前没有见过的新概念,然后执行 serverless.yml 文件的演练。所有函数处理程序的演练将是冗余的。因此,我们将只演练一个函数处理程序。您可以将理解其他函数作为练习。

项目结构

现在,如果您查看项目结构,lambda 函数处理程序都位于 todos 文件夹中的单独 .py 文件中。serverless.yml 文件在每个函数处理程序的路径中指定 todos 文件夹。没有外部依赖项,因此也没有 requirements.txt 文件。

新概念

现在,您可能第一次看到一些术语。让我们快速浏览一下:

  • **dynamoDB** - 这是 AWS 提供的 NoSQL(不仅仅是 SQL)数据库。虽然不完全准确,但广义地说,NoSQL 之于 SQL 就如同 Word 之于 Excel。您可以在这里阅读更多关于 NoSQL 的信息。有四种类型的 NoSQL 数据库:文档数据库、键值数据库、列式存储和图数据库。dynamoDB 是一个键值数据库,这意味着您可以将键值对不断插入到数据库中。这类似于 redis 缓存。您可以通过引用其键来检索值。

  • **boto3** - 这是 Python 的 AWS SDK。如果您需要在 lambda 函数中配置、管理、调用或创建任何 AWS 服务(EC2、dynamoDB、S3 等),则需要 boto3 SDK。您可以在这里阅读更多关于 boto3 的信息。

除此之外,在 serverless.yml 和处理程序函数的演练过程中,我们将遇到一些概念。我们将在那里讨论它们。

serverless.yml 演练

serverless.yml 文件以服务的定义开头。

service: serverless-rest-api-with-dynamodb

接下来是通过以下行声明框架版本范围:

frameworkVersion: ">=1.1.0 <=2.1.1"

这就像一个检查。如果您的 serverless 版本不在此范围内,它将抛出错误。当您共享代码并希望每个人使用此 serverless.yml 文件都使用相同的 serverless 版本范围以避免问题时,这很有帮助。

接下来,在 provider 中,我们看到了两个我们之前没有遇到过的额外字段:**environment** 和 **iamRoleStatements**。

provider:
   name: aws
   runtime: python3.8
   environment:
      DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
   iamRoleStatements:
      - Effect: Allow
         Action:
         - dynamodb:Query
         - dynamodb:Scan
         - dynamodb:GetItem
         - dynamodb:PutItem
         - dynamodb:UpdateItem
         - dynamodb:DeleteItem
         Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:
         *:table/${self:provider.environment.DYNAMODB_TABLE}"

**Environment**,正如您可能猜到的那样,用于定义环境变量。在此 serverless.yml 文件中定义的所有函数都可以获取这些环境变量。我们将在下面的函数处理程序演练中看到一个示例。在这里,我们将 dynamoDB 表名称定义为环境变量。

$ 符号表示变量。self 关键字指代 serverless.yml 文件本身,而 opt 指的是我们在 sls deploy 命令中提供的选项。因此,表名将是服务名称后面跟一个连字符,再跟文件找到的第一个阶段参数: serverless deploy 命令期间可用的选项之一,或提供程序阶段(默认为 dev)。因此,在这种情况下,如果您在 serverless deploy 命令期间未提供任何选项,则 dynamoDB 表名为 serverless-rest-api-with-dynamodb-dev。您可以此处了解更多关于 serverless 变量的信息。

iamRoleStatements 定义赋予函数的权限。在本例中,我们允许函数对 dynamoDB 表执行以下操作:QueryScanGetItemPutItemUpdateItemDeleteItem。资源名称指定允许执行这些操作的确切表。如果您在资源名称位置输入了 "*",则表示允许对所有表执行这些操作。但是,这里我们只想允许对一个表执行这些操作,因此,使用标准 arn 格式在资源名称中提供了此表的 arn(Amazon 资源名称)。同样,这里使用 serverless deploy 命令期间指定的选项区域(如果存在)或提供程序中提到的区域(默认为 us-east-1)中的第一个。

在 functions 部分,函数按照标准格式定义。请注意,get、update、delete 都有相同的路径,id 作为路径参数。但是,每种方法都不同。

functions:
   create:
      handler: todos/create.create
      events:
         - http:
            path: todos
            method: post
            cors: true
   list:
      handler: todos/list.list
      events:
         - http:
            path: todos
            method: get
            cors: true
   get:
      handler: todos/get.get
      events:
         - http:
            path: todos/{id}
            method: get
            cors: true

   update:
      handler: todos/update.update
      events:
         - http:
            path: todos/{id}
            method: put
            cors: true
   delete:
      handler: todos/delete.delete
      events:
         - http:
            path: todos/{id}
            method: delete
            cors: true

稍后,我们会遇到一个以前从未见过的块,即 resources 块。此块基本上可帮助您在 CloudFormation 模板中指定函数运行所需创建的资源。在本例中,我们需要创建一个 dynamoDB 表才能使函数正常运行。到目前为止,我们已经指定了表名,甚至引用了它的 ARN。但是我们还没有创建表。在 resources 块中指定表的特性将为我们创建该表。

resources:
   Resources:
      TodosDynamoDbTable:
         Type: 'AWS::DynamoDB::Table'
         DeletionPolicy: Retain
         Properties:
         AttributeDefinitions:
            -
               AttributeName: id
               AttributeType: S
         KeySchema:
            -
               AttributeName: id
               KeyType: HASH
         ProvisionedThroughput:
            ReadCapacityUnits: 1
            WriteCapacityUnits: 1
            TableName: ${self:provider.environment.DYNAMODB_TABLE}

这里定义了许多配置,其中大部分是特定于 dynamoDB 的。简而言之,我们要求 serverless 创建一个名为 'TodosDynamoDbTable' 的资源,类型为 'DynamoDB Table',TableName(在底部提到)等于提供程序环境变量中定义的名称。我们将它的删除策略设置为 'Retain',这意味着如果堆栈被删除,则资源将被保留。参见此处。我们声明该表将具有一个名为 id 的属性,其类型为字符串。我们还指定 id 属性将是 HASH 密钥或分区密钥。您可以此处了解更多关于 dynamoDB 表中 KeySchemas 的信息。最后,我们指定表的读取容量和写入容量。

就是这样!我们的 serverless.yml 文件已准备就绪。现在,由于所有函数处理程序都大致相似,我们将仅介绍 create 函数的处理程序。

create 函数处理程序的演练

我们从几个导入语句开始

import json
import logging
import os
import time
import uuid

接下来,我们导入 boto3,如上所述,它是 Python 的 AWS SDK。我们需要 boto3 来在 lambda 函数中与 dynamoDB 交互。

import boto3
dynamodb = boto3.resource('dynamodb')

接下来,在实际的函数处理程序中,我们首先检查 'events' 有效负载的内容(create API 使用 post 方法)。如果它的主体不包含 'text' 密钥,则表示我们没有收到要添加到待办事项列表中的有效项目。因此,我们将引发异常。

def create(event, context):
   data = json.loads(event['body'])
   if 'text' not in data:
      logging.error("Validation Failed")
      raise Exception("Couldn't create the todo item.")

假设我们按预期获得了 'text' 密钥,我们将准备将其添加到 dynamoDB 表中。我们获取当前时间戳,并连接到 dynamoDB 表。请注意如何获取 serverless.yml 中定义的环境变量(使用 os.environ)

timestamp = str(time.time())
table = dynamodb.Table(os.environ['DYNAMODB_TABLE'])

接下来,我们通过使用 uuid 包生成随机 uuid,使用接收到的数据作为文本,将 createdAt 和 updatedAt 设置为当前时间戳,并将字段 'checked' 设置为 False 来创建要添加到表中的项目。'checked' 是另一个字段,除了文本之外,您还可以使用 update 操作更新它。

item = {
   'id': str(uuid.uuid1()),
   'text': data['text'],
   'checked': False,
   'createdAt': timestamp,
   'updatedAt': timestamp,
}

最后,我们将项目添加到 dynamoDB 表中,并将创建的项目返回给用户。

# write the todo to the database
table.put_item(Item=item)

# create a response
response = {
   "statusCode": 200,
   "body": json.dumps(item)
}
return response

通过此演练,我认为其他函数处理程序将不言自明。在某些函数中,您可能会看到此语句:"body" − json.dumps(result['Item'], cls=decimalencoder.DecimalEncoder)。这是针对json.dumps 中的一个错误的解决方法。json.dumps 默认情况下无法处理十进制数,因此,创建了decimalencoder.py文件来包含处理此问题的 DecimalEncoder 类。

恭喜您理解了使用 serverless 创建的第一个综合项目。项目的创建者还在README文件中分享了他的部署端点以及测试这些函数的方法。请查看。继续下一章以查看另一个示例。

无服务器 - Telegram 回声机器人

这是官方 serverless 项目列表中提供的另一个有趣的项目。我们基本上是在 Telegram 上创建一个新的机器人,然后使用 set_webhook 方法将其连接到我们的 lambda 函数,并在 lambda 函数中编写使机器人回显它接收到的任何消息的代码。

先决条件

您需要在手机或桌面上安装 Telegram 应用程序。下载选项可此处找到。Telegram 是一款消息应用程序,类似于 WhatsApp 或 Messenger。安装应用程序后,您需要在应用程序中创建一个新的机器人。为此,请点击“新建消息”图标(右下角的圆形铅笔图标),然后搜索 BotFather。点击已验证的帐户。

Telegram

与 BotFather 开始聊天后,创建新机器人非常简单明了。您发送 \newbot 命令,输入机器人的名称和用户名,您将获得一个访问令牌,您需要记下它。

Newbot

代码演练

代码可在 GitHub 上找到 - https://github.com/serverless/examples/tree/master/aws-python-telegram-bot

我们将查看项目结构,然后执行 serverless.yml 文件和 handler.py 文件的演练。

项目结构

我们可以看到,此项目有一个外部依赖项(python telegram bot 库),列在 requirements.py 中:

python-telegram-bot==8.1.1

package.json 和 serverless.yml 都显示已使用 serverless-python-requirements 插件来捆绑 python 需求(在本例中为 telegram bot 库)。因此,README.md 文件还建议您执行 npm install 以安装必要的插件。我个人建议您删除 package.json,并使用 sls plugin install -n serverless-python-requirements 安装 serverless-python-requirements。这将自动创建 package.json 文件。这也将确保您安装最新版本的 serverless-python-requirements。通过运行 npm install,您将安装现有 package.json 中提到的版本,该版本可能已过期。

如果您阅读 README.md 文件,您会发现一个被引用的文件实际上并不存在于项目中:serverless.env.yml。您需要创建此文件并在其中输入您的 TELEGRAM_TOKEN。这是出于安全原因考虑。TELEGRAM_TOKEN 应该保密,您不希望公开共享它。因此,此项目的创建者没有在 GitHub 上添加 serverless.env.yml 文件。但是您需要在本地机器上创建它。

serverless.yml 演练

serverless.yml 文件以服务的定义开头。

service: serverless-telegram-bot

接下来,定义提供程序。这里再次设置了一个环境变量。此变量 (TELEGRAM_TOKEN) 的值是从您应该在本地创建的 serverless.env.yml 文件中获取的。同样,我们使用 $ 来表示变量。

provider:
   name: aws
   runtime: python3.6
   profile: ckl
   environment:
      TELEGRAM_TOKEN: ${file(./serverless.env.yml):TELEGRAM_TOKEN, ''}

functions 块非常简单明了。定义了两个函数,都是 HTTP 触发的。但是,http 事件参数在这里用一行代码定义。

- http:
   path: /set_webhook
   method: post

使用的单行执行替换为 - httpPOST /set_webhook

另外,请注意 webhookset_webhook 函数都在同一个处理程序文件中。

functions:
   webhook:
      handler: handler.webhook
      events:
         - http: POST /
set_webhook:
   handler: handler.set_webhook
   events:
      - http: POST /set_webhook

最后,定义 serverless-python-requirements 插件。

plugins:
   - serverless-python-requirements

handler.py 的演练

我们从几个导入语句开始

import json
import telegram
import os
import logging

接下来,定义一个日志记录器对象,它基本上可以帮助我们输入日志语句。请注意,这对于 python 运行时函数不是必需的。简单的 print 语句也会被记录。

唯一的区别是日志器的输出包括日志级别、时间戳和请求 ID。您可以此处了解更多关于 logging 库的信息。

# Logging is cool!
logger = logging.getLogger()
if logger.handlers:
   for handler in logger.handlers:
      logger.removeHandler(handler)
logging.basicConfig(level=logging.INFO)

接下来,定义 OK_RESPONSE 和 ERROR_RESPONSE 的 JSON。它们用作函数的返回值。

OK_RESPONSE = {
   'statusCode': 200,
   'headers': {'Content-Type': 'application/json'},
   'body': json.dumps('ok')
}
ERROR_RESPONSE = {
   'statusCode': 400,
   'body': json.dumps('Oops, something went wrong!')
}

接下来,定义两个 API 函数使用的辅助函数。此函数使用在 serverless.yml 中作为环境变量提供的令牌返回一个机器人实例。

def configure_telegram():
   """
   Conimages the bot with a Telegram Token.
   Returns a bot instance.
   """
   TELEGRAM_TOKEN = os.environ.get('TELEGRAM_TOKEN')
   if not TELEGRAM_TOKEN:
      logger.error('The TELEGRAM_TOKEN must be set')
      raise NotImplementedError
   return telegram.Bot(TELEGRAM_TOKEN)

接下来,定义两个 API 的处理程序函数。让我们首先看看 set_webhook 函数。在这里,从我们前面看到的 configure_telegram 函数中获取机器人实例。接下来,从标头中提取 host 字段,并从传入事件的 requestContext 块中提取 stage 字段。使用这两个字段,构造 webhook 的 URL。最后,使用 bot.set_webhook(url) 函数将其应用于机器人。如果 webhook 设置正确,则设置 OK_RESPONSE,否则设置 ERROR_RESPONSE。请注意,此 set_webhook API 必须使用 POSTMAN 等工具手动触发一次。

def set_webhook(event, context):
   """
   Sets the Telegram bot webhook.
   """
   logger.info('Event: {}'.format(event))
   bot = configure_telegram()
   url = 'https://{}/{}/'.format(
      event.get('headers').get('Host'),
      event.get('requestContext').get('stage'),
   )
   webhook = bot.set_webhook(url)

   if webhook:
      return OK_RESPONSE
   return ERROR_RESPONSE

让我们了解`set_webhook`函数如何获取正确的Webhook URL。请注意,`set_webhook`函数和`webhook`函数的路径仅相差`/set_webhook`。它们共享相同的host和stage。因此,我们可以使用`set_webhook`函数事件中接收到的host和dev来推导出`webhook`函数的端点。如果您的端点是'https://abcdefghijk.execute-api.us-east-1.amazonaws.com/dev',那么host将是'https://abcdefghijk.execute-api.us-east-1.amazonaws.com',stage将是'dev'。`set_webhook`函数由'https://abcdefghijk.execute-api.us-east-1.amazonaws.com/dev/set_webhook'触发,而`webhook`函数由'https://abcdefghijk.execute-api.us-east-1.amazonaws.com/dev'触发。因此,`set_webhook`事件中的参数可以帮助我们构造`webhook`函数的端点URL。

最后,让我们看看`webhook`函数。它非常简单。它从configure_telegram辅助函数接收bot实例。然后它检查事件。如果它是一个POST事件并且包含主体,则它从主体中提取聊天ID和消息。如果文本是'/start',表示对话开始,它将使用bot.sendMessage(chat_id=chat_id, text=text)命令回复标准问候语。否则,它将回复它接收到的相同文本。

def webhook(event, context):
   """
   Runs the Telegram webhook.
   """
   bot = configure_telegram()
   logger.info('Event: {}'.format(event))

   if event.get('httpMethod') == 'POST' and event.get('body'): 
      logger.info('Message received')
      update = telegram.Update.de_json(json.loads(event.get('body')), bot)
      chat_id = update.message.chat.id
      text = update.message.text
      
      if text == '/start':
         text = """Hello, human! I am an echo bot, built with Python and the Serverless Framework.
         You can take a look at my source code here: https://github.com/jonatasbaldin/serverless-telegram-bot.
         If you have any issues, please drop a tweet to my creator: https://twitter.com/jonatsbaldin. Happy botting!"""

      bot.sendMessage(chat_id=chat_id, text=text)
      logger.info('Message sent')

      return OK_RESPONSE
   return ERROR_RESPONSE

一旦您通过POSTMAN之类的工具触发了`set_webhook`函数,您就可以在Telegram上打开您的机器人并与它聊天。它将按预期回显消息。

Set WebHook

恭喜您创建了您的第一个Telegram机器人!

广告