使用-Zappa-构建-Python-无服务器-Web-服务-全-

使用 Zappa 构建 Python 无服务器 Web 服务(全)

原文:zh.annas-archive.org/md5/3c97e70c885487f68835a4d0838eee09

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书基于以现代方式开发基于 Python 的无服务器 Web 或微服务的方式。无服务器涉及云服务提供商提供的无服务器基础设施。本书展示了如何使用亚马逊网络服务来实现无服务器基础设施。此外,它还涵盖了使用 Zappa 的部署过程。Zappa 消除了手动干预,为您提供了自动化的部署方式,并帮助您维护多个部署阶段。

本书适合谁

本书适用于初学者到有经验的 Python 开发人员,他们想要了解在无服务器基础设施上开发 Python Web 服务或微服务的方式。有经验的 Python 开发人员可以通过学习无服务器技术和了解无服务器部署来提升他们的技能。

本书涵盖的内容

第一章,用于无服务器的亚马逊网络服务,涵盖了理解 AWS Lambda 和 API Gateway 服务的基础知识。还介绍了通过与 AWS 控制台和 CLI 工具交互创建无服务器服务的手动过程。

第二章,开始使用 Zappa,解释了 Zappa 工具的概念,并详细说明了使用 Zappa 相对于 AWS 服务的手动过程的好处。

第三章,使用 Zappa 构建 Flask 应用程序,探讨了基本的 Flask 应用程序开发,并使用 Zappa 作为无服务器应用程序进行部署。

第四章,使用 Zappa 构建基于 Flask 的 REST API,介绍了基于 Flask 的 RESTful API 开发和使用 Zappa 的部署过程。

第五章,使用 Zappa 构建 Django 应用程序,讨论了 Django 核心应用程序开发,并使用 Zappa 将应用程序部署为 AWS Lambda 上的无服务器应用程序。

第六章,使用 Zappa 构建 Django REST API,专注于使用 Django REST 框架实现 RESTful API 以及使用 Zappa 的部署过程。

第七章,使用 Zappa 构建 Falcon 应用程序,带您了解使用 Falcon 框架开发 RESTful API 作为微服务的过程,以及使用 Zappa 的部署过程。

第八章,使用 SSL 的自定义域,解释了如何使用 Zappa 配置自定义域,并涵盖了使用 AWS 生成 SSL。

第九章,在 AWS Lambda 上执行异步任务,展示了使用 Zappa 执行耗时任务的异步操作的实现。

第十章,高级 Zappa 设置,让您熟悉 Zappa 工具的附加设置,以增强应用部署过程。

第十一章,使用 Zappa 保护无服务器应用程序,概述了使用 Zappa 在 AWS Lambda 上保护无服务器应用程序的安全方面。

第十二章,使用 Docker 的 Zappa,介绍了在 AWS Lambda 上下文环境中使用 Docker 容器化进行应用程序开发。

充分利用本书

在开始之前,读者需要一些先决条件。读者应具备以下条件:

  • 对虚拟环境有很好的理解

  • 理解 Python 包安装

  • 了解使用 Apache 或 NGINX 进行传统部署的知识

  • 对 Web 服务或微服务的基本了解

下载示例代码文件

您可以从www.packtpub.com的账户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,文件将直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的以下工具解压或提取文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Building-Serverless-Python-Web-Services-with-Zappa。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自丰富书籍和视频目录的其他代码包可供下载,网址为github.com/PacktPublishing/。快去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/BuildingServerlessPythonWebServiceswithZappa_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“Zappa 部署需要生成zappa_settings.json文件,该文件生成zappa init命令。”

代码块设置如下:

client = boto3.client('lambda')
response = client.invoke(
    FunctionName='MyFunction',
    InvocationType='Event'
)

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

$ curl https://quote-api.abdulwahid.info/daily
{"quote": "May the Force be with you.", "author": "Star Wars", "category": "Movies"}

任何命令行输入或输出都以以下方式编写:

$ pip install awscli

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单中的单词或对话框中的单词会在文本中以这种方式出现。这是一个例子:“单击“创建函数”按钮。”

警告或重要说明会出现在这样的地方。

提示和技巧会出现在这样的地方。

第一章:用于无服务器的亚马逊网络服务

在本章中,我们将学习关于亚马逊网络服务管理无服务器基础架构。我们将探索 AWS 工作流程以创建无服务器应用程序。我们将学习手动创建基本无服务器应用程序的过程,以及使用 AWS CLI 进行自动化处理的过程。

本章我们将涵盖的主题包括:

  • 从传统服务器过渡到无服务器

  • 开始使用 AWS Lambda

  • AWS Lambda 的工作原理

  • 执行 Lambda 函数

  • 创建 Lambda 触发器

  • 创建无服务器 RESTful API

  • 通过 AWS CLI 与 AWS Lambda 进行交互

技术要求

在继续之前,有一些技术先决条件。我们将通过 Web 控制台和 AWS CLI 演示 AWS。应考虑以下先决条件:

  • 所有演示都在 Ubuntu 16.04 的 Linux 机器上进行了测试。我们已经分享了本书中使用的每个库的链接。您可以获取有关特定平台的安装和配置的详细信息。

  • 我们使用开源库和软件。因此,对于每个库,我们将分享其官方文档链接。您可以参考这些链接以获取有关特定库的详细信息。

从传统服务器过渡到无服务器

自从 Web 托管开始以来,Web 托管发生了巨大变化。物理服务器机器被多个 Web 应用程序共享,当需要扩展时,这是一个真正的挑战。对于任何个人或公司来说,购买整个服务器机器来托管其 Web 应用程序都非常昂贵。

但是,由于虚拟化,任何 Web 应用程序都不需要物理服务器。虚拟化提供了创建许多虚拟服务器的能力,而不是单个物理服务器。

现在,无服务器的新时代使开发人员的生活变得更加轻松,因为我们可以将辛勤工作集中在开发上,而不是花时间和金钱在部署上。

亚马逊推出了亚马逊弹性计算云(Amazon EC2)作为云计算解决方案。亚马逊 EC2 使得在亚马逊云中创建一系列虚拟服务器或实例成为可能,而无需投资于硬件。您可以根据网络、计算和存储的需求进行扩展。

无服务器方法只是消除设置托管环境的手动工作量的过程。云服务提供商提供无服务器服务,因此您实际上从未拥有任何服务器。相反,云服务提供商在高可用性基础设施中执行您的代码。

开始使用 AWS Lambda

许多云服务提供商为无服务器基础架构引入了不同的服务。亚马逊推出了 AWS Lambda 作为计算服务,您只需提供代码,AWS Lambda 就会在高度可扩展的基础设施中执行代码。您无需担心手动管理服务。您需要支付代码执行的计算时间,当您的代码未运行时则不收费。

AWS Lambda 根据需要执行代码,以响应诸如 S3 存储桶上的数据存储事件、Amazon DynamoDB 事件和通过 API Gateway 的 HTTP 请求事件等事件。AWS Lambda 能够根据 AWS CloudWatch Events 的计划时间事件执行代码。AWS Lambda 支持 Python、Node.js、C#和 Java。

亚马逊简单存储服务(S3)是亚马逊提供的存储服务。它具有一个简单的 Web 界面来存储数据。亚马逊 S3 具有可供其他服务使用的相关服务事件。

AWS Lambda 的工作原理

您需要编写一个函数,该函数将由 AWS Lambda 代表您执行。

AWS Lambda 是基于容器的模型实现的,支持运行时环境,并根据 Lambda 函数的配置执行代码。当调用 Lambda 函数时,它会根据 AWS Lambda 配置启动容器(执行环境),并启用基本的运行时环境,这是执行代码所需的。

让我们从一些实际工作开始:

  1. 要创建一个 Lambda 函数,您必须拥有 AWS 账户。如果您没有 AWS 账户,那么您需要在 AWS 上注册(aws.amazon.com/),提供一些基本的联系和付款信息,因为这是亚马逊所需的基本信息。

  2. 转到 Lambda 主页(console.aws.amazon.com/lambda/home)。单击“创建函数”按钮。这将重定向您到创建函数页面,下一步将描述该页面。请查看以下屏幕截图:

  1. AWS 提供了三种不同的选项来创建 Lambda 函数,比如从头开始创建、蓝图和无服务器应用程序存储库。我们将使用蓝图选项,其中包含一些内置的 Lambda 函数。我们可以根据搜索栏中的要求选择这些蓝图,您可以通过标签和属性进行过滤,或者通过关键字搜索:

  1. 让我们选择一个 hello-world-python 蓝图。一旦我们选择了蓝图,我们需要设置关于 Lambda 函数的基本信息。这些信息包括 Lambda 函数的名称和角色,如下面的屏幕截图所示:

  1. 在这里,名称将是您的 Lambda 函数的唯一标识,而角色定义了您的 Lambda 函数的权限。

有三种选项可用于创建角色:

  • 选择现有角色

  • 从模板创建新角色

  • 创建自定义角色

让我们更详细地看一下它们:

  • 选择现有角色:这允许您选择先前创建的角色。

  • 从模板创建新角色:在这里,您需要定义一个角色名称。AWS Lambda 提供了预先配置的内置角色策略模板,具有预配置的权限。这些权限基于 AWS Lambda 函数所需的其他 AWS 服务相关权限。在任何角色选择上,Lambda 将自动将日志记录权限添加到 CloudWatch(AWS 日志记录服务),因为这是 Lambda 所需的基本权限。

  • 创建自定义角色:AWS 提供了额外的权限来创建一个定制的角色来访问 AWS Lambda。在这里,您可以根据自己的需求定义角色。

  1. 让我们创建一个带有某些角色的HelloWorld Lambda 函数。在这里,我选择了 S3 对象只读权限策略模板。

  2. 以下屏幕截图描述了新创建的HelloWorld Lambda 函数:

HelloWorld Lambda 函数

Lambda 函数包括三个部分:

  • 配置

  • 触发器

  • 监控

让我们来看一下关于配置和监控的详细信息。我们将为触发器设置一个单独的部分。

配置

Lambda 执行取决于配置设置。配置 Lambda 函数需要以下详细信息:

  • 函数代码

  • 环境变量

  • 标签

  • 执行角色

  • 基本设置

  • 网络

  • 调试和错误处理

函数代码

在这里,您需要编写代码。Lambda 函数有一个预定义的编写代码的模式。在编写代码时,您需要理解上下文。Lambda 提供了三种可行性,这决定了代码的运行时执行:

  • 代码输入类型:此部分提供了三种选项来决定代码的输入类型,比如内联编辑代码、上传 ZIP 文件和从 Amazon S3 上传文件。

  • 运行时:此部分提供了选项来决定代码的运行时编程语言上下文,比如 Python、C#、NodeJS 和 Java。

  • 处理程序:处理程序定义了您的方法/函数的路径,例如<filename>.<method_name>。例如,如果您想要执行一个名为handler的函数,该函数在main.py中定义,那么它将是main.handler

让我们回到我们新创建的名为lambda_handler的 hello world 函数。

在这里,处理程序的值被定义为lambda_function.lambda_handler,其中lambda_function.py是文件名,lambda_handler是方法名:

def lambda_handler(event, context): 
    print("value1 = " + event['key1']) 
    print("value2 = " + event['key2']) 

Lambda_handler接受两个位置参数,eventcontext

  • event:此参数包含与事件相关的信息。例如,如果我们配置 Lambda 函数与 Amazon S3 存储桶事件,那么我们将在事件参数中获得 S3 存储桶信息,例如存储桶名称,区域等。

  • context:此参数包含可能在运行时需要的与上下文相关的信息,以供代码执行。

环境变量

您可以以键值对的形式设置环境变量,这些变量可以在您的代码中使用。

标签

您可以使用标签对 Lambda 函数进行分组和过滤。您可能有多个具有不同区域的 Lambda 函数,因此标签有助于使 Lambda 函数更易管理。

执行角色

正如我们之前讨论的,在创建 Lambda 函数时角色和权限,Lambda 提供了编辑您在创建 Lambda 函数时选择的现有角色的能力。

基本设置

在基本设置下,您可以配置内存和执行超时。Lambda 支持的内存范围从 128 MB 到 1,536 MB。超时执行以秒为单位;Lambda 支持的默认超时执行时间为 300 秒。此设置可帮助您控制 Lambda 函数的代码执行性能和成本。

网络

在网络部分,您可以配置对 Lambda 函数的网络访问。

AWS 提供了VPC(虚拟私有云)服务,用于创建允许访问 AWS 服务的虚拟网络。您还可以根据自己的需求配置网络。

我们将在接下来的章节中讨论带有 VPC 的 Lambda 函数。目前,我们将在网络部分选择无 VPC。

调试和错误处理

AWS Lambda 会自动重试失败的异步调用。但是您也可以配置DLQ(死信队列),例如 SQS 队列或 SNS 主题。要配置 DLQ,Lambda 函数必须具有访问 DLQ 资源的权限。

既然我们了解了配置,让我们继续执行 Lambda 函数。

让我们看一下监控部分,它描述了与我们的 Lambda 函数相关的活动。它可以用于分析我们的 Lambda 函数执行的性能。

监控

AWS CloudWatch 是 AWS 资源的监控服务,并管理所有活动日志。它创建指标数据以生成统计数据。CloudWatch 实现了对 AWS 资源的实时监控。它还监视与 AWS EC2 或 RDS 数据库实例以及其他资源相关的硬件信息。

Lambda 监控部分显示与 Lambda 函数活动和性能相关的最近 24 小时的分析数据。以下屏幕截图显示了我们的 hello world Lambda 函数的监控分析信息:

让我们继续下一节,我们将看一下 Lambda 函数的执行。

执行 Lambda 函数

AWS Lambda 支持多种执行方法。让我们从其自己的 Web 控制台界面开始基本执行。AWS Lambda 提供了手动测试函数的能力,您可以在其中定义测试事件上下文。如果您想针对其他 Amazon 服务进行测试,则有内置的事件模板可用。

以下屏幕截图演示了测试事件的创建:

如前面的屏幕截图所示,单个 Lambda 函数最多可以有 10 个测试事件,并且测试事件是持久的,因此您可以在需要测试 Lambda 函数时重复使用它们。

我使用事件名称HelloWorld创建了测试事件,现在我将执行HelloWorld函数,将 Lambda 函数转换为 Python 微服务,如下所示:

from __future__ import print_function 
import json 

print('Loading function') 

def lambda_handler(event, context): 
    print("Received event: " + json.dumps(event, indent=2)) 
    print("value1 = " + event['key1']) 
    print("value2 = " + event['key2']) 
    print("value3 = " + event['key3']) 
    return "Hello World" 

在这里,我们打印事件数据,然后返回到Hello World字符串:

Lambda 在每个请求执行上管理一些信息,例如请求 ID 和计费信息。Lambda 价格模型是基于请求处理的时间消耗,而请求 ID 是每个请求的唯一标识。

在日志输出中,您可以看到所有的打印语句输出。现在,让我们引发一个错误,看看 Lambda 如何响应并返回日志。

我们将用以下片段替换当前代码:

from __future__ import print_function 
import json 

print('Loading function') 

def lambda_handler(event, context): 
    print("Received event: " + json.dumps(event, indent=2)) 
    raise Exception('Exception raised manually.') 

以下屏幕截图是执行结果的日志片段:

在这里,Lambda 以完整的堆栈跟踪信息做出了响应,并将其记录下来。您可以检查 CloudWatch 日志,因为 CloudWatch 已预先配置了 AWS Lambda 执行。

我们从 Lambda 控制台了解了 Lambda 函数的执行,现在是时候从计划触发器执行 Lambda 函数了。在我们的项目中,我们经常需要有一个 cron 作业计划,在特定时间段执行一些功能。

Lambda 触发器将帮助我们根据事件设置触发器。让我们继续介绍如何向我们的 hello world 函数引入触发器。

创建 Lambda 触发器

Lambda 函数可以根据事件进行配置。AWS 提供了支持许多事件的触发器列表。这些触发器属于它们关联的 AWS 服务。

您可以从触发器部分向 Lambda 函数添加触发器。

我将稍微修改 hello world Lambda 函数。在这里,我们打印请求 ID,该 ID 作为aws_request_id属性在上下文对象中接收。它还打印时间戳:

现在,我们将向我们的 Lambda 函数添加一个触发器,该触发器将每分钟执行我们的 Lambda 函数。

以下屏幕截图显示了“添加触发器”流程,在这里您可以轻松地从左侧面板配置任何触发器与您的 Lambda 函数:

我们将配置 CloudWatch Events 触发器。CloudWatch Events 提供描述 AWS 资源更改的几乎实时系统事件。

您可以设置简单的事件规则,以在发生 AWS 资源的操作事件时进行操作,并且还可以安排自动事件,根据 cron 或速率表达式自触发。

cron 和速率表达式是定义计划表达式的两种不同方法。cron 表达式有六个必填字段,如 cron(字段),而速率表达式有两个必填字段,如速率(值单位)。这些方法帮助我们定义计划表达式。您可以在docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html找到详细信息。

在这里,我们将安排一个速率表达式,以便每分钟执行我们的 hello world Lambda 函数。我们需要从触发器下拉菜单中选择 CloudWatch Events。

为了创建 CloudWatch 事件规则,我们将创建一个新规则。我们需要使用一些必需的信息设置规则,例如规则名称,这是一个唯一标识符。因此,我们将规则命名为hello-world-every-minute,规则类型为事件模式或计划表达式。在我们的情况下,它将是一个计划表达式,速率(1 分钟),如前面的屏幕截图所示。

一旦我们设置了触发器并启用它,按照计划表达式,计划事件将被触发。让我们在五分钟后查看我们的 hello world Lambda 日志。

要查看与任何服务相关的日志,您需要执行以下操作:

  1. console.aws.amazon.com/cloudwatch/上打开 CloudWatch 控制台

  2. 在导航窗格中,选择日志

  3. 选择与HelloWorld Lambda 函数相关的日志组

以下屏幕截图描述了 CloudWatch 日志访问:

通过选择HelloWorld Lambda 函数日志组,您可以看到与我们的HelloWorld Lambda 函数相关的日志活动。以下屏幕截图显示了HelloWorld函数的日志:

在这里,您可以看到我们的 hello world Lambda 函数自从我们启用了触发器以来每分钟执行一次。

现在,让我们继续创建一个无服务器的 RESTful API。

无服务器 RESTful API

让我们了解微服务场景,我们将部署一个无服务器的 hello world 函数,通过 API Gateway 响应 HTTP 事件。

Amazon API Gateway 服务使您能够创建、管理和发布与任何规模的 AWS 资源交互的 RESTful API。API Gateway 提供了一个接口,通过该接口您可以通过 REST 应用程序编程接口公开后端。

为了启用 AWS 无服务器基础设施,API Gateway 发挥着重要作用,因为它可以配置为执行 Lambda 函数。

现在,我们将配置一个 API Gateway 服务来执行 Lambda 函数

这是 hello world 函数:

当我们将 AWS Lambda 函数与 API Gateway 集成时,Lambda 函数必须返回一个带有所需键的字典对象,如statusCodeheadersbodybody属性的值必须是 JSON 字符串。因此,我们将 Python 字典转换为 JSON 字符串。

现在是时候将 API Gateway 与 Lambda 函数集成了。正如我们在之前关于触发器的讨论中所看到的,我们将使用 API Gateway 添加一个触发器:

我们将创建一个名为LambdaMicroservice的 API Gateway 服务。API Gateway 使您能够根据需要创建和维护部署阶段。

如果您想保护您的 API,那么您有两个选项——使用 AWS IAM 并使用访问密钥打开它,或者保持它开放,使其公开可用。

AWS IAM(身份访问管理)是 AWS 云服务,有助于创建安全访问凭据以访问 AWS 云服务。

使用访问密钥功能允许您从 API Gateway 控制台生成密钥。在我们的情况下,我们只会保持安全性开放,因为我们需要公开访问我们的 API:

一旦添加并保存更改,REST API 将在几秒钟内准备就绪。调用 URL 就是我们的 REST API 端点。

让我们使用curl命令行工具访问调用 URL 并查看发生了什么:

$ curl https://cfi6872cxa.execute-api.us-east-2.amazonaws.com/prod/HelloWorld
{"message": "Hello World returned in JSON"}

就是这样。我们已经使用 AWS Lambda 和 API Gateway 创建了一个无服务器的 RESTful API。现在,我们将看看如何使用 AWS CLI 与 AWS 服务进行交互。

AWS Lambda 与 AWS CLI 的交互

AWS CLI 是在 AWS SDK for Python 的基础上开发的开源工具,使用 Boto 库提供与 AWS 服务交互的命令。通过非常少的配置,您可以从 CLI 管理任何 AWS 服务。它提供对 AWS 服务的直接访问,您可以开发 shell 脚本来管理您的资源。

例如,如果您想将文件上传到 S3 存储桶,那么您只需通过 CLI 执行一个命令即可完成:

$ aws s3 cp index.html s3://bucket-name/ 

aws s3 cp是一个类似 shell 的命令,执行多部分文件上传操作以完成操作。

它还支持对一些 AWS 服务进行定制。您可以使用--help命令查看aws-cli支持的 AWS 服务列表。

安装 AWS CLI

awscli可用作 Python 分发包。您可以使用pip命令轻松安装它,如下面的代码所述:

$ pip install awscli --upgrade  

以下是先决条件:

  • Python 2 的版本为 2.6.5+或 Python 3 的版本为 3.3+

  • Unix、Linux、macOS 或 Windows

配置 AWS CLI

awscli直接访问 AWS 服务,但我们需要配置和验证它以访问 AWS 服务。

运行aws configure命令以配置 AWS CLI 与您的 Amazon 帐户:

您可以从“My Security Credentials”选项中获取 AWS 访问密钥 ID 和 AWS 秘密访问密钥,如下图所示:

让我们使用 AWS CLI 配置 AWS Lambda 函数。

使用 AWS CLI 配置 Lambda 函数

让我们使用awscli 实用程序命令配置我们的 hello world Lambda 函数和触发器。

AWS CLI 支持所有可用的 AWS 服务。您可以使用aws help查看aws命令的详细描述,并列出所有可用的服务。

我们对 Lambda 感兴趣,因为我们将创建一个具有简单 hello world 上下文的 Lambda 函数:

$ aws lambda help  

这将列出 Lambda 服务的完整描述以及管理 AWS Lambda 服务所需的所有可用命令。

创建 Lambda 函数

在这里,我们将使用aws lambda create-function命令创建一个新的 Lambda 函数。要运行此命令,我们需要传递必需和可选参数。

确保您具有具有lambda:CreateFunction操作权限的角色。

以前,在 AWS Lambda 控制台中,我们选择了内联编辑作为代码入口点。现在,我们将使用 ZIP 文件作为部署包。

在创建 Lambda 函数之前,我们应该创建一个 Lambda 函数部署包。

此部署包将是一个 ZIP 文件,其中包含您的代码和任何依赖项。

如果您的项目有一些依赖关系,则必须在项目的根目录中安装依赖关系。例如:

 $ pip install requests -t <project-dir> OR  
 $ pip install -r requirements.txt  -t <project-dir> 

这里,-t选项表示目标目录。

在名为handler.py的文件中创建一个简单的lambda_handler函数,如下图所示:

现在,让我们制作一个 ZIP 文件作为部署包,其中包含前面的代码:

现在,我们准备创建 Lambda 函数。以下截图描述了命令执行:

您可以看到,在 AWS Lambda 控制台中,Lambda 函数立即被创建:

让我们讨论我们在aws lambda create-function命令中使用的必需和可选参数:

  • --function-name(必需):名称不言自明。我们需要传递要创建的 Lambda 函数的名称。

  • --role(必需):这是一个必需的参数,我们需要将 AWS 角色 ARN 用作值。确保此角色具有创建 Lambda 函数的权限。

  • --runtime(必需):我们需要提到 Lambda 函数执行的运行时环境。正如我们之前提到的,AWS Lambda 支持 Python、Node.js、C#和 Java。因此,这些是可能的值:

  • python2.7

  • python3.6

  • nodejs

  • nodejs4.3

  • nodejs6.10

  • nodejs4.3-edge

  • dotnetcore1.0

  • java8

  • --handler(必需):在这里,我们提到将作为 AWS Lambda 执行入口点的函数路径。在我们的情况下,我们使用了handler.lambda_function,其中处理程序是包含lambda_function的文件。

  • --description:此选项允许您添加有关 Lambda 函数的一些文本描述。

  • --zip-file:此选项用于从本地环境/计算机上传代码的部署包文件。在这里,您需要在 ZIP 文件路径前添加fileb://作为前缀。

  • --code:此选项可帮助您从 AWS S3 存储桶上传部署包文件。

您应该传递一个带有模式的字符串值,例如这里所示的一个:

 "S3Bucket=<bucket-name>,S3Key=<file-name>,S3ObjectVersion=<file-version-id>". 

还有许多其他可选参数,您可以通过help命令查看,例如aws lambda create-function help. 您可以根据自己的需求使用它们。

现在我们将看到使用命令$ aws lambda invoke调用 Lambda 函数。

调用函数

Lambda CLI 提供了一个命令来直接调用 Lambda 函数:

$ aws lambda invoke --function-name <value> <outfile> 

让我们来看看这些参数:

  • --function-name(必需):此参数要求输入 Lambda 函数名称

  • outfile(必需):在这里,您需要提到一个文件名,返回的输出或 Lambda 函数的响应将被存储在那里

这里还有其他可选参数,您可以通过help命令列出。

让我们调用我们最近创建的HelloWorldCLI函数:

当我们调用 Lambda 函数时,它立即以状态代码做出响应,并且 Lambda 函数返回的输出数据存储在新创建的lambda_output.txt文件中,通过lambda invoke命令。

创建事件源映射

这是aws lambda命令的一个子命令,用于为您的 Lambda 函数创建事件映射。$ aws lambda create-event-source-mapping仅支持 Amazon Kinesis 流和 Amazon DynamoDB 流事件映射。我们将在即将到来的章节中讨论使用 Zappa 进行 Amazon API Gateway 和 CloudWatch 事件的事件映射。

总结

在本章中,我们学习了创建简单的 AWS Lambda 的手动过程,并配置了一些触发器。此外,我们还使用 AWS CLI 查看了 AWS Lambda 的配置。实现无服务器应用程序真是令人惊讶。这些 AWS 服务在创建无服务器基础架构中扮演着重要的角色,您可以在其中开发应用程序并将其部署为无服务器。

问题

  1. 无服务器的好处是什么?

  2. Amazon S3 在无服务器基础架构中的作用是什么?

第二章:使用 Zappa 入门

之前,我们学习了如何使用 AWS Web 控制台和 AWS CLI 创建无服务器应用程序。现在,我们将学习 Zappa 和自动化操作,以创建无服务器应用程序。

在本章中,我们将涵盖以下主题:

  • 什么是 Zappa?

  • 安装和配置 Zappa

  • 使用 Zappa 构建、测试和部署 Python Web 服务

  • Zappa 的用途

技术要求

在继续之前,让我们确保满足技术要求。接下来的小节将介绍本章的硬件和软件要求。

硬件

为了演示目的,我们使用了一个基本配置的机器,具体规格如下:

  • 内存—16GB

  • 处理器—Intel Core i5

  • CPU—2.30GHz x 4

  • 图形—Intel HD Graphics 520

软件

以下是软件规格:

  • OS—Ubuntu 16.04 LTS

  • OS 类型—64 位

  • Python 3.6

  • Python 开发包:build-essentialpython-devpython-virtualenv

  • AWS 凭据和 AWS CLI

  • Zappa

我们将在接下来的章节中详细描述设置环境的过程。与此同时,你可以配置诸如python3.6awscli等必要的软件包。

什么是 Zappa?

Zappa 是一个开源工具,由 Gun.io 的创始人/CTO Rich Jones 开发和设计(www.gun.io/)。Zappa 主要设计用于在 AWS Lambda 和 API Gateway 上构建和部署无服务器 Python 应用程序。

Zappa 非常适合使用 Flask 和 Bottle 等框架部署无服务器 Python 微服务,用于托管大型 Web 应用程序和 CMSes 的 Django。你也可以部署任何 Python WSGI 应用程序。

在上一章中,我们使用 AWS Lambda 和 API Gateway 实现了基本的 hello world 微服务。Zappa 自动化了所有这些手动流程,并为我们提供了一个方便的工具来构建和部署 Python 应用程序。

就像这样简单:

$ pip install zappa
$ zappa init
$ zappa deploy

正如我们之前所描述的,传统的 Web 托管是指服务器需要始终在线,监听 HTTP 请求并逐个处理请求。如果传入的 HTTP 请求队列增长,那么服务器将无法每秒处理那么多请求,就会出现超时错误。

API Gateway 使用虚拟 HTTP 服务器为每个请求提供自动扩展。这就是它可以在不失败的情况下为数百万个请求提供服务的原因。因此,我们可以在当前部署成本的零停机侵害下获得无限扩展。

现在,我们将进行一个应用程序演示,但在此之前,让我们在你的机器上配置 Zappa,我们将在接下来的章节中介绍。

安装和配置 Zappa

安装 Zappa 是一项简单的任务,但在继续之前,我们需要配置先决条件。确保你有 Python 2.7 或 Python 3.6,并且有一个有效的 AWS 账户。现在,你需要在你的机器上使用help awscli配置 AWS 凭据:

$ pip install awscli

使用aws configure命令配置 AWS 凭据,如下截图所示:

配置 AWS 凭据要求你有 AWS 访问密钥 ID、AWS 秘密访问密钥、默认区域名称和默认输出格式。

你可以从“我的安全凭据”页面获取 AWS 凭据信息,如下截图所示:

一旦你配置了 AWS 凭据,我们就可以继续安装 Zappa 了。

Zappa 必须安装在虚拟环境中。强烈建议在安装 Zappa 之前创建一个虚拟环境并激活它。我倾向于使用virtualenv工具。还有其他可用于管理虚拟环境的工具:

$ virtualenv env -p python3.6

在这里,我们创建了一个名为env的虚拟环境,并使用python3.6,其中-p表示 Python 版本。现在,按照以下步骤激活虚拟环境:

$ source env/source/bin

现在我们已经准备好了,让我们使用pip安装 Zappa:

$ pip install zappa

现在,我们准备启动 Zappa。在接下来的章节中,我们将创建一个小程序,演示如何使 Zappa 的部署变成无服务器。

使用 Zappa 构建、测试和部署 Python Web 服务

我们将使用 Python 的 Bottle 框架创建一个简单的 hello world 程序作为微服务。让我们按照一些基本步骤来配置一个使用 Bottle 框架的小项目:

  1. 首先,我们将创建一个名为lambda_bottle_poc的新项目目录:
$ mkdir lambda_bottle_poc
  1. 让我们在lambda_bottle_poc目录中创建一个虚拟环境:
$ virtualenv env -p python3.6
  1. 以下是使用 Bottle 框架的基本 hello world 程序:

现在是时候将程序部署为 AWS Lambda 上的无服务器,并通过 API Gateway 公开/hello API。在上一章中,我们描述了使用 AWS 控制台和 AWS CLI 手动部署 Python 应用程序的过程,这是一个非常大的挑战。

但借助 Zappa,所有 AWS 控制台和 AWS CLI 的手动流程都被自动化,并提供了一种快速的方式来在无服务器环境中部署和维护您的应用程序。

构建部署包

让我们使用zappa init命令初始化 Zappa。这个命令可以帮助您创建和部署 Python 应用程序。这个命令以用户交互模式运行,并提出一些基本问题,以便我们可以设置部署过程。

问卷结束时,Zappa 创建了一个名为zappa_settings.json的 JSON 文件。这个文件实际上就是 Zappa 的支撑,因为它维护了 Zappa 内部使用的映射信息。

我们将在接下来的几分钟内详细讨论 Zappa 的init命令过程。在那之前,先看一下以下截图,描述了zappa init命令的流程:

正如您所看到的,zappa init启动了用户交互模式,并提出了一些问题。让我们看看每个问题的一些信息。

您想如何称呼这个环境?(默认开发)

Amazon API Gateway 提供了一种机制来维护托管 API 的不同环境阶段。例如,您可以为开发、暂存和生产创建环境阶段。

借助 Zappa,您可以非常轻松地管理环境阶段。对于前面的问题,您可以定义自己的环境阶段名称,或者将其留空以考虑默认的阶段名称为dev

你想给你的存储桶起什么名字?(默认 zappa-2o2zd8dg4)

Zappa 部署将需要上传到私有的 Amazon S3 存储桶。AWS Lambda 需要两种类型的代码输入,即内联代码和上传 ZIP 文件。如果 ZIP 文件大小超过 10MB,则考虑将 ZIP 文件上传到 Amazon S3。这就是为什么 Zappa 默认创建一个存储桶,用于上传部署 ZIP 文件并引用 AWS Lambda。

您可以提供自己现有的存储桶名称,或者选择由 Zappa 建议的默认名称。如果存储桶不存在,Zappa 将自动为您创建一个。

你的应用功能的模块化路径是什么?(默认开发)

AWS Lambda 函数需要一个属性,比如lambda_handler,它指向一个函数作为 Lambda 执行的入口点。因此,我们需要提供有关函数名称的信息,以模块化路径的形式,例如<filename>.<function_name/app_name>给 Zappa。

在我们的案例中,我们有一个名为hello.py的文件,以及使用 Python Bottle 框架的Bottle类创建的应用对象。因此,对于这个问题的答案是hello.app

您想全球部署应用程序吗?(默认否)

AWS 提供了一个功能,可以将 Lambda 服务扩展到所有可用的区域。如果您希望使您的服务全球可用,并且延迟更少,那就是您应该做的。Zappa 支持这个功能,它将使您能够在所有区域扩展 Lambda 服务,而无需任何手动操作。

最后,您将得到一个zappa_settings.json文件,其中包含了与您的部署相关的所有配置。让我们在下一节中查看zappa_settings.json文件。

zappa_settings.json 文件

完成问卷调查后,Zappa 会根据您的输入创建一个基本的zappa_settings.json文件。zappa_settings.json在配置 Zappa 与您的项目时起着重要作用。如果您在现有项目(Django/Flask/Pyramid/Bottle)中初始化 Zappa,那么 Zappa 会自动检测项目类型,并相应地创建zappa_settings.json文件。

以下是我们新创建的zappa_settings.json文件的内容,用于 hello world 程序:

{
   "dev": {
       "app_function": "hello.app",
       "aws_region": "ap-south-1",
       "profile_name": "default",
       "project_name": "lambda-bottle-p",
       "runtime": "python3.6",
       "s3_bucket": "zappa-2o2zd8dg4"
   }
}

对于 Django 项目,它使用django_settings而不是app_functiondjango_settings需要用您的 Django 设置的路径进行初始化:

{
   "dev": {
       "django_settings": "your_project.settings",
       "aws_region": "ap-south-1",
       "profile_name": "default",
       "project_name": "lambda-bottle-p",
       "runtime": "python3.6",
       "s3_bucket": "zappa-2o2zd8dg4"
   }
}

上述配置足以部署一个基本的 Python Web 应用程序。让我们继续部署 hello world 作为无服务器应用程序。

部署和测试 hello world

Zappa 部署非常简单,因为您只需要运行一个命令来开始部署:

$ zappa deploy <stage_name>

就是这样!我们已经完成了部署。现在,让我们部署 hello world 程序。以下截图描述了部署过程:

部署完成后,我们会得到 API URL 端点。让我们通过访问/hello端点的 API URL 来测试 hello world 应用程序:

$ curl -l  https://071h4br4e0.execute-api.ap-south-1.amazonaws.com/dev/hello

运行上述命令后,您将看到以下输出:

Hello World!

能够在几秒钟内配置服务并部署真是太神奇了。现在,我们将详细了解与zappa_settings.json文件相关的基本用法。

基本用法

Zappa 涵盖了每一个部署过程。让我们详细讨论一下 Zappa 的部署流程。

初始部署

一旦您初始化了 Zappa,您就可以通过单个命令将应用程序部署到production阶段,如下面的代码片段所示:

$ zappa deploy production
.
.
.
Deployment complete ! https://071h4br4e0.execute-api.ap-south-1.amazonaws.com/production

当您调用$ zappa deploy命令时,Zappa 会执行一些任务来完成部署。以下是 Zappa 关于部署的内部流程和过程:

  1. 通过将本地环境中的应用程序代码压缩成 ZIP 存档文件,并用预编译的 Lambda 包中的版本替换任何依赖项。

  2. 使用所需的 WSGI 中间件设置 Lambda handler函数。

  3. 将前两个步骤生成的存档上传到 Amazon S3 存储桶。

  4. 创建和管理必要的 AWS IAM 策略和角色。

  5. 使用上传到 AWS S3 存储桶的 ZIP 存档文件创建 AWS Lambda 函数。

  6. 根据 Zappa 配置创建 AWS API Gateway 资源以及不同的阶段。

  7. 为 API Gateway 资源创建 WSGI 兼容路由。

  8. 将 API Gateway 路由链接到 AWS Lambda 函数。

  9. 最后,从 AWS S3 中删除 ZIP 文件。

注意:lambda-packagesgithub.com/Miserlou/lambda-packages)是由 Zappa 社区维护的开源存储库。该存储库包含了最基本的 Python 库作为预编译的二进制文件,这些文件将与 AWS Lambda 兼容。

这就是 Zappa 处理部署过程的方式——它会自行完成所有这些任务,并让您通过一个命令来部署您的应用程序。

更新

如果您已经部署了应用程序,那么您只需使用以下命令简单地在 AWS Lambda 上更新最新的应用程序代码:

$ zappa update production
.
.
.
Your updated Zappa deployment is live!: https://071h4br4e0.execute-api.ap-south-1.amazonaws.com/production

我们可以将其与仅更新一些任务的zappa deploy进行比较。它们在这里提到:

  • 它使用最新的应用程序代码创建一个存档 ZIP;本地环境是一个预编译的 Lambda 包

  • 将存档的 ZIP 上传到 AWS S3

  • 更新 AWS Lambda

就是这样!我们已经完成了更新现有的部署,而且只花了几秒钟。

状态

你可以通过运行以下命令简单地检查应用程序部署的状态:

$ zappa status production

这将打印有关 AWS Lambda 函数、计划事件和 API Gateway 的详细信息。

尾随日志

Zappa 提供了一个查看与部署相关日志的功能。你可以简单地使用以下命令:

$ zappa tail production

这将打印与 HTTP 请求和 AWS 事件相关的所有日志。如果你想打印与 HTTP 请求相关的日志,你可以简单地传递--http参数:

$ zappa tail production --http

你可以通过简单地使用以下代码来撤销前面的命令,从而反转非 HTTP 事件和日志消息:

$ zappa tail production --non-http

你还可以使用--since参数限制日志的时间:

$ zappa tail production --since 1h # 1 hour
$ zappa tail production --since 1m # 1 minute
$ zappa tail production --since 1mm # 1 month  

你还可以使用--filter参数过滤日志,例如:

$ zappa tail production --since 1h --http --filter “POST”

这将仅显示最近一小时的 HTTPPOST请求。这使用 AWS CloudWatch 日志过滤器模式(docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html)。

回滚

AWS Lambda 维护你的部署的修订版。你可以通过提供一个修订号来回滚到先前部署的版本,如下所示:

$ zappa rollback production -n 3

这将简单地将 Lambda 代码恢复到先前上传的归档 ZIP。

取消部署

如果你想完全删除你部署的应用程序,那么你只需使用以下命令:

$ zappa undeploy production

这将删除发布的 AWS Lambda 和 API Gateway。如果你想要与你的应用程序相关的 AWS CloudWatch 日志,那么你只需在前面的命令中传递参数,如下所示:

$ zappa undeploy production --remove-logs

这将清除 AWS CloudWatch 中的日志。

Zappa 提供了一个命令,在不部署应用程序的情况下在本地生成一个构建包归档:

$ zappa package production

当你运行这个命令时,Zappa 会自动将你的活动虚拟环境打包成 AWS Lambda 兼容的包。

在内部,它用 AWS Lambda 兼容的、预编译版本替换任何本地依赖。这些依赖按以下顺序包括:

  • Lambda 兼容许多来自本地缓存的 Linux wheels

  • Lambda 兼容许多来自 PyPi 的 Linux wheels

  • Lambda 包中的 Lambda 特定版本(github.com/Miserlou/lambda-packages

  • 归档活动虚拟环境

  • 归档项目目录

在处理、打包和打包时,Zappa 会忽略一些不必要的文件,比如.pyc文件。如果它们可用,那么.py将被忽略。Zappa 还设置正确的执行权限,配置包设置,并创建一个唯一的、可审计的包清单文件。

生成的包归档将与 Lambda 兼容。你可以设置一个回调函数,一旦创建了归档,它将被调用:

{
    "production": {
       "callbacks": {
            "zip": "my_app.zip_callback"
        }
    }
 }

在这里,production 是你的舞台名称,在回调中,你可以通过映射到"zip"来设置回调方法。这可以帮助你编写自己的自定义部署自动化。

我们已经看到了 Zappa 的基本用法。现在是时候做一些实际的工作了。我们将使用 Zappa 构建一些 Python 应用程序开发,敬请关注!

摘要

Zappa 提供了灵活的功能,让你可以执行部署过程。我们介绍了 Zappa 的基本用法,并了解了打包和部署过程。Zappa 让开发人员可以非常简单和容易地配置和执行应用程序到无服务器环境的部署。

问题

  1. Zappa 是什么?

  2. 我们如何在 AWS 中保护应用程序?

第三章:使用 Zappa 构建 Flask 应用程序

在上一章中,我们了解了使用 Zappa 自动化部署过程的重要性,因为 Zappa 帮助我们在 AWS 无服务器基础架构上部署 Python 应用程序。我们使用它来开发使用一些 Python Web 框架的 Python 应用程序。在本章中,我们将开发一个基于 Flask 的应用程序,作为 AWS Lambda 上的无服务器应用程序。

在上一章中,我们了解了 Zappa 如何有助于执行无服务器部署,以及如何通过单个命令轻松部署。现在,是时候看到 Zappa 部署的更大型应用程序了,因为看到应用程序如何配置并移动到 AWS Lambda 是非常重要的。

在本章中,我们将涵盖以下主题:

  • 什么是 Flask?

  • 最小的 Flask 应用程序

  • 与 Zappa 配置

  • 在 AWS Lambda 上构建,测试和部署

  • 一个完整的 Flask Todo 应用程序

技术要求

在继续之前,让我们了解技术要求并配置开发环境。本章中有一个应用程序开发的概念演示。因此,有一些先决条件:

  • Ubuntu 16.04/macOS/Windows

  • Python 3.6

  • Pipenv 工具

  • Zappa

  • Flask

  • Flask 扩展

一旦您配置了 Python 3.6 并安装了 Pipenv 工具,您可以创建一个虚拟环境并安装这些软件包。我们将在后面的章节中探索其安装和配置。让我们继续了解一些基于 Python 框架及其相关实现的基本概念。

什么是 Flask?

Flask 是 Python 社区中知名的微型 Web 框架。它因其可扩展的特性而被广泛采用和青睐。Flask 旨在保持代码简单但可扩展。

默认情况下,Flask 不包括任何数据库抽象层,表单验证或任何其他特定功能。相反,Flask 支持扩展以向您的应用程序添加任何明确定义的功能。有许多扩展可用于提供数据库集成,表单验证,文件上传处理,身份验证等。Flask 核心团队审查扩展,并确保它们不会破坏未来的发布。

Flask 允许您根据应用程序的需要定义设计。您不必遵循 Flask 的一些严格规则。您可以将应用程序代码编写在单个文件中,也可以以模块化的方式编写。Flask 支持内置开发服务器和快速调试器,单元测试,RESTful 请求分发,Jinja2 模板化和安全的 cookies(用于客户端会话),所有这些都符合 WSGI 1.0 标准和基于 Unicode。

这就是为什么许多 Python 社区的人更喜欢将 Flask 框架作为他们的首选。让我们继续前进,探索基于 Flask 的应用程序开发过程,实际实现以及无服务器方法。

安装 Flask

Flask 主要依赖于两个外部库,即 Werkzeug 和 Jinja2。Werkzeug 提供了 Python 标准的 WSGI(Web 服务器网关接口),使 Python 应用程序能够与 HTTP 交互。Jinja2 是一个模板引擎,使您能够使用自定义上下文呈现 HTML 模板。

现在,让我们继续安装 Flask。所有其依赖项将自动安装;您无需手动安装依赖项。

建议您使用virtualenv来安装 Flask,因为virtualenv使您能够为不同的 Python 项目并行安装 Python 软件包。

如果您没有virtualenv,那么您可以使用以下代码简单安装它:

$ sudo apt-get install python-virtualenv

一旦您安装了virtualenv,您需要为您的 Flask 项目创建一个新的环境,如下面的屏幕截图所示:

我们将在接下来的章节中使用virtualenv。现在,让我们安装 Flask:

$ pip install flask

我们准备好开始使用 Flask 了。我们将创建一个最小的 Flask 应用程序来演示 Flask 应用程序的工作流程。

一个最小的 Flask 应用程序

让我们看看最小的 Flask 应用程序是什么样子的:

from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
  return 'Hello World!'

就是这样,我们完成了最小的 Flask 应用程序。使用 Flask 配置和创建微服务非常简单。

让我们讨论一下前面的代码到底在做什么,以及我们如何运行这个程序:

  1. 首先,我们导入了一个 Flask 类。

  2. 接下来,我们创建了一个 Flask 类的实例。这个实例将是我们的 WSGI 应用程序。第一个参数将是模块或包的名称。在这里,我们创建了一个单一的模块,因此我们使用了__name__。这是必需的,这样 Flask 就知道在哪里查找模板、静态和其他目录。

  3. 然后,我们使用app.route作为装饰器,带有 URL 名称作为参数。这将定义并映射路由到指定的函数。

  4. 该函数将被调用以处理路由装饰器中指定的 URL 的 HTTP 请求。

要运行这个程序,您可以使用flask命令或python -m flask,但在此之前,您需要设置一个环境变量FLASK_APP,并指定 Flask 实例所在的模块文件名:

$ export FLASK_APP=hello_world.py
$ flask run
* Serving Flask app "flask_todo.hello_world"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

这启动了一个内置服务器,足够用于本地测试和调试。以下是浏览器中运行的本地主机的截图:

当然,这在生产环境下是行不通的,但 Flask 提供了许多部署选项。您可以查看flask.pocoo.org/docs/0.12/deploying/#deployment获取更多信息,但在我们的情况下,我们将使用 Zappa 在 AWS Lambda 和 API Gateway 上部署到无服务器环境。

使用 Zappa 进行配置

为了配置 Zappa,需要安装 Zappa,如前一章所述。Zappa 提供了zappa init命令,它可以启用用户交互模式初始化,以便我们可以配置 Python 应用程序。

我遵循了zappa init命令建议的默认配置设置。这会生成zappa_settings.json文件,这是配置任何 Python 应用程序与 Zappa 的基础。

以下是zappa_settings.json文件的内容:

{
  "dev": {
      "app_function": "hello_world.app",
      "aws_region": "ap-south-1",
      "profile_name": "default",
      "project_name": "flask-todo",
      "runtime": "python3.6",
      "s3_bucket": "zappa-yrze3w53y"
  }
}

现在,在初始化期间,Zappa 有能力识别您的 Python 应用程序的类型,并相应地生成设置属性。在我们的情况下,Zappa 检测到 Python 程序是一个 Flask 应用程序。因此,它要求 Flask 实例路径,我们在hello_world.py文件中初始化为app = Flask(__name__)

现在 Zappa 配置已经按照我们的基本需求完成,是时候在 AWS Lambda 上部署它了。

在 AWS Lambda 上构建、测试和部署

我们在前一章描述了 Zappa 的基本用法和一些基本命令。使用这些命令,我们可以构建部署包、部署应用程序和执行其他基本操作。

一旦您在zappa_settings.json文件中设置了所有有效的属性,您就可以使用zappa deploy <stage_name>命令开始部署过程。根据我们的zappa_settings.json文件,我们定义了一个名为dev的阶段,因此,要开始部署,我们可以运行deploy命令,如下面的代码所示:

$ zappa deploy dev

以下截图描述了部署流程:

一旦 Zappa 部署完成,它会生成一个随机的 API 网关端点。Zappa 根据zappa_settings.json文件配置 AWS Lambda 与 API Gateway。

现在,Flask 应用程序可以通过先前生成的 API 访问。让我们测试一下,看看 Flask 应用程序的 Hello World!响应。您可以在浏览器中输入 URL,如下面的截图所示:

现在,让我们继续下一节,看看如何使用 Flask 框架开发应用程序。

一个完整的 Flask Todo 应用程序

由于我们已经看到 Zappa 如何轻松部署 Flask 应用程序,现在是时候看看在开发基于 Flask 的应用程序时可能需要的完整工作流程。我们将开发一个基于 Flask 的模块化应用程序,其中每个功能都将是一个独立的模块,例如认证、待办应用程序等。

认证模块将负责维护认证和授权机制。它还将包括登录和注册过程的实现。

todo模块将有一个基本的 todo 操作实现,这个操作流程将由认证模块授权。借助 Flask 扩展,我们将管理和配置这些模块。除了这些核心模块,我们还将看到与用户界面、数据库配置和静态文件集成相关的实现。

先决条件

为了设置开发环境,我们需要执行一些与virtualenv和所需包相关的配置。

Virtualenv

在我们开始项目工作之前,让我们创建一个虚拟环境并启用它,如下面的截图所示:

Flask 扩展

Flask 是一个微框架,但它具有可扩展的特性,您可以根据需要添加更多功能。为了开发一个待办应用程序,我们可能需要一些基本功能,如数据持久性和用户认证机制。因此,在开发 Flask 应用程序时,我们将使用一些 Flask 扩展。

Flask 注册表提供了许多扩展,这些扩展是独立的包,您可以轻松地将它们配置到您的 Flask 应用程序实例中。您可以在flask.pocoo.org/extensions/上看到完整的 Flask 扩展列表。

我们将使用以下 Flask 和 Flask 扩展包:

  • Flask==0.12.2

  • Flask-Login==0.4.0

  • Flask-SQLAlchemy==2.3.2

  • Flask-WTF==0.14.2

  • Flask-Migrate==2.1.1

我建议将这些包列在一个名为requirements.txt的单独文件中,然后一次性安装它们,如下所示:

pip install -r requirements.txt

这将安装所有列出的包及其依赖项。

脚手架

在从头开始实现任何项目时,您可以自由设计项目的脚手架。我们将遵循以下截图中显示的脚手架:

让我们详细看看每个目录及其用途:

  • .env:这是我们的virtualenv目录,是通过virtualenv命令创建的。

  • auth:我们将使用Flask-LoginFlask-SqlAlchemy扩展创建一个独立的通用认证模块。

  • config:在这里,我们将创建一些配置和通用数据库模型,其他模块可能需要这些模型。

  • static:将静态内容放在static目录下是 Flask 的标准做法。因此,我们将使用这个目录来存放所有需要的静态内容。

  • templates:Flask 内置支持 Jinja2 模板引擎,并遵循基于模块名称的模板文件的标准布局。我们将在实际使用模板时详细描述这一点。

  • todo:这是一个独立的 Flask 模块或包,具有基本的待办功能。

  • __init__.py:这是 Python 的标准文件,必须在目录下构建 Python 包。我们将在这里编写代码来配置我们的应用程序。

  • migrations:这个目录是由Flask-Migrate自动生成的。在后面的部分中,我们将看到Flask-Migrate的工作原理。

  • .gitignore:这个文件包含了应该被 Git 版本控制忽略的文件和目录列表。

  • LICENSE:我使用 GitHub 创建了一个 Git 存储库,并为我们的flask_todo存储库包含了 MIT 许可证。

  • README.md:这个文件用于在 GitHub 上描述有关存储库的信息。

  • requirements.txt:这是我们列出了在前面部分提到的所有所需包的文件。

  • run.py:在这里,我们将创建我们的 Flask 应用的最终实例。

  • zappa_settings.json:这个文件是由 Zappa 生成的,包含了与 Zappa 相关的配置。

我们将在接下来的部分详细解释代码。

配置

在实施任何项目时,我们可能需要一些特定于不同环境的配置,例如在开发环境中切换调试模式和在生产环境中监控。

Flask 有一种灵活的方式来克服配置处理机制。Flask 在其实例上提供了一个config对象。这个config对象是通过扩展 Python 的dictionary对象构建的,但具有一些额外的功能,如从文件、对象和默认内置配置加载配置。您可以在flask.pocoo.org/docs/0.12/config/上查看config机制的详细描述。

为了根据环境维护配置,我们将创建一个名为config/config.py的文件,其中包含以下代码:

import os
from shutil import copyfile

BASE_DIR = os.path.dirname(os.path.dirname(__file__))

def get_sqlite_uri(db_name):
    src = os.path.join(BASE_DIR, db_name)
    dst = "/tmp/%s" % db_name
    copyfile(src, dst)
    return 'sqlite:///%s' % dst

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(24)
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True
    SQLALCHEMY_RECORD_QUERIES = True
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = get_sqlite_uri('todo-dev.db')

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = get_sqlite_uri('todo-prod.db')

config = {
    'dev': DevelopmentConfig,
    'production': ProductionConfig,
}

在这里,我们创建了一个Config对象作为一个具有一些通用配置和Flask-SqlAlchemy配置的基类。然后,我们用特定于环境的类扩展了基本的Config类。最后,我们创建了一个映射对象,我们将从上述键中使用。

基本模型

SQLAlchemy 最著名的是其对象关系映射器ORM),这是一个可选组件,提供了数据映射器模式,其中类可以以多种方式映射到数据库中,允许对象模型和数据库模式从一开始就以一种清晰的解耦方式发展。我们在这里使用Flask-SQLAlchemy扩展,它扩展了对 SQLAlchemy 的支持。Flask-SQLAlchemy增强了可能需要与 Flask 应用集成的功能。

我们将组合使用Flask-SQLAlchemy所需的通用 SQL 操作。因此,我们将创建一个基本模型类,并将使用这个类来创建其他模块的模型类。这就是我们将其放在config目录下的原因。这是models.py文件。

文件—config/models.py

from app import db

class BaseModel:
    """
    Base Model with common operations.
    """

    def delete(self):
        db.session.delete(self)
        db.session.commit()

    def save(self):
        db.session.add(self)
        db.session.commit()
        return self

您可以在这里看到,我们将所有模型都需要的数据库操作分组在一起。db实例是在app/__init__.py文件中使用Flask-SQLAlchemy扩展创建的。

在这里,我们实现了savedelete方法。db.Model定义了一个通用模式,用于创建代表数据库表的模型类。为了保存和删除,我们需要遇到一些预定义的操作,如db.session.add()db.session.delete()db.session.commit()

因此,我们将通用操作分组在savedelete方法下。这些方法将从一个模型类中调用,该模型类将继承它们。我们将在稍后创建一个模型类时再详细介绍。

认证

为了开发一个认证模块,我们将使用Flask-Login扩展。Flask-Login扩展提供了用户会话管理机制。它处理管理用户会话的常见任务,如登录、注销和记住用户。

要集成Flask-Login,您需要创建实例并定义一些默认参数,如下面的代码片段中所述:

from flask_login import LoginManager
app = Flask(__name__)
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
login_manager.login_message_category = "info"
login_manager.init_app(app)

我们将创建一个认证模块作为一个auth包。auth包将具有基本的脚手架,如下所示:

蓝图

在深入描述每个文件之前,让我们看一下 Flask 实例化机制。正如你已经知道的,我们正在创建一个独立模块作为root模块下的一个子模块。Flask 引入了蓝图的概念,用于将子模块组件放在一个共同的模式下。

Flask 蓝图实例非常类似于 Flask 实例,但它不是一个应用程序对象。相反,它具有构建和扩展父应用程序的能力。借助蓝图,您可以设计一个模块化的应用程序。

Blueprint instantiation in the auth/__init__.py file:
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views

如您所见,它具有与Flask类非常相似的特征,并遵循类似的模式。现在,我们将在视图中使用blueprintauth实例来注册路由。要执行应用程序,我们需要将blueprint对象与 Flask 应用程序实例绑定。

app/__init__.py file where we are going to create the Flask application instance:
from .auth import auth as auth_blueprint
from app.config import config

app = Flask(__name__)
app.config.from_object(config[environment])

app.register_blueprint(auth_blueprint, url_prefix='/auth')

借助register_blueprint方法,我们正在注册auth模块蓝图,我们还可以添加 URL 前缀。在查看todo模块解释之后,我们将对此文件进行完整描述。

模型

让我们从创建具有基本功能的User模型开始。以下是用户模型的代码片段。

文件—auth/models.py

import re
from datetime import datetime

from app.config.models import BaseModel
from flask_login.mixins import UserMixin
from sqlalchemy.orm import synonym
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
from app import login_manager

class User(UserMixin, BaseModel, db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    _email = db.Column('email', db.String(64), unique=True)
    password_hash = db.Column(db.String(128))

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)

    def __repr__(self):
        return '<User {0}>'.format(self.email)

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        if not len(email) <= 64 or not bool(re.match(r'^\S+@\S+\.\S+$', email)):
            raise ValueError('{} is not a valid email address'.format(email))
        self._email = email

    email = synonym('_email', descriptor=email)

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        if not bool(password):
            raise ValueError('no password given')

        hashed_password = generate_password_hash(password)
        if not len(hashed_password) <= 128:
            raise ValueError('not a valid password, hash is too long')
        self.password_hash = hashed_password

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    def to_dict(self):
        return {
            'email': self.email
        }

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

现在,我们已经创建了User模型,但它如何与Flask-Login扩展相关联或映射呢?答案是load_user方法,该方法由login_manager.user_loader装饰器包装。Flask 提供了这个方法来将用户加载到会话中。该方法使用会话中存在的user_id进行调用。

我们可以通过User模型将用户数据持久化到数据库中。作为一个 Web 应用程序,用户数据需要通过用户界面(如 HTML)输入。根据我们的需求,我们需要两种类型的 HTML 表单,用于登录和注册功能。

让我们继续下一节,学习通过 Flask 渲染 HTML 表单。

表单

Flask-WTF扩展提供了在 Flask 中开发表单并通过 Jinja2 模板渲染它们的能力。Flask-WTF扩展了WTForms库,该库具有设计表单的标准模式。

我们需要两个表单,如SignupFormLoginForm。以下是创建表单类的代码。

文件—auth/forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import Required, Length, Email, EqualTo

class LoginForm(FlaskForm):
    email = StringField(
        'Email', validators=[Required(), Length(1,64), Email()]
    )
    password = PasswordField(
        'Password', validators=[Required()]
    )
    submit = SubmitField('Log In')

class SignupForm(FlaskForm):
    email = StringField(
        'Email', validators=[Required(), Length(1,64), Email()]
    )
    password = PasswordField(
        'Password', validators=[
            Required(),
            EqualTo('confirm_password', message='Password must match.')]
    )
    confirm_password = PasswordField(
        'Confirm Password', validators=[Required()]
    )
    submit = SubmitField('Sign up')

在这里,我们创建了一些带有验证的表单。现在,我们将在视图部分中使用这些表单,在那里我们将呈现模板以及表单上下文。

视图

Flask 以一种非常灵活的方式实现了视图,您可以在其中定义路由。Flask 的通用视图实现受到 Django 的通用视图的启发。我们将在后面的部分详细描述方法视图,但在这里,我们将使用简单的视图。

以下是视图片段。

文件—auth/views.py

from flask import render_template, redirect, url_for
from flask_login import login_user, login_required, logout_user

from app.auth import auth
from app.auth.forms import LoginForm, SignupForm
from app.auth.models import User

@auth.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user_by_email = User.query.filter_by(email=form.email.data).first()
        if user_by_email is not None and user_by_email.verify_password(form.password.data):
            login_user(user_by_email)
            return redirect(url_for('todo.list'))
    return render_template('auth/login.html', form=form)

@auth.route('/signup', methods=['GET', 'POST'])
def signup():
    form = SignupForm()
    if form.validate_on_submit():
        if not User.query.filter_by(email=form.email.data).scalar():
            User(
                email = form.email.data,
                password = form.password.data
            ).save()
            return redirect(url_for('auth.login'))
        else:
            form.errors['email'] = 'User already exists.'
            return render_template('auth/signup.html', form=form)
    return render_template('auth/signup.html', form=form)

@auth.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('auth.login'))

在这里,我们创建了/login/signup/logout路由,我们根据 HTTP 请求调用它们。我们在 HTTP GET请求上呈现一个空的表单实例,并在POST请求上通过使用Flask-WTF方法和validate_on_submit()方法处理数据。在呈现模板时,我们传递表单实例并根据需要的操作进行重定向。

让我们在下一节中看一下模板机制。

模板

Flask 内置了对 Jinja2 模板的支持。Jinja2 模板具有用于呈现 HTML 的标准定义模式。我们可以通过传递上下文参数来放置动态内容。Jinja2 提供了使用一些表达式和条件、扩展和包含模板功能来呈现 HTML 的能力。

Flask 遵循标准的模板搭建结构来布置所有模板文件。以下是我们遵循的搭建结构,通过在项目根目录下创建一个templates目录,然后根据其他模块名称创建子目录:

在这里,我们根据模块创建了模板,并将通用模板放在根目录下。

同样,我们保持了静态文件的脚手架:

我们保留了静态库和模块相关文件。借助url_for方法,我们可以获取任何静态文件和路由的相对路径。因此,在以下模板中,我们使用url_for方法包含了所有静态文件,例如<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css')}}">

同样,我们将在基本模板中包含所有静态文件。

文件—templates/base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <meta name="author" content="AbdulWahid AbdulHaque">
    <title>Flask Todo App</title>

    <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css.map') }}">
    {% block css %}{% endblock %}

    <script type="text/javascript" src="img/jquery-3.3.1.min.js')}}"></script>
    <script type="text/javascript" src="img/bootstrap.min.js')}}"></script>
    <script type="text/javascript" src="img/popper.min.js')}}"></script>
    {% block js %}{% endblock %}
</head>
<body>
    {% include 'navbar.html' %}
    {% block body %}{% endblock %}
    <script type="text/javascript">
        $('.dropdown-toggle').dropdown();
    </script>
</body>
</html>

我们定义了所有其他模板所需的通用 HTML。我们还创建了一个基本的 bootstrap 导航栏,并将其保存在navbar.html中,通过{% include 'navbar.html' %}包含在base.html模板中。正如你所看到的,Jinja2 模板使得维护模板和提供标准模式变得非常容易。

navbar.html template where we created a navbar using Bootstrap CSS classes.

文件—templates/navbar.html

<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">Todo's</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNavDropdown">
        {% if current_user.is_authenticated %}
        <ul class="navbar-nav ml-auto">
            <li class="nav-item dropdown ml-auto">
                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    Welcome <i>{{ current_user.email }}</i>
                </a>
                <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
                    <a class="dropdown-item" href="../auth/logout">Logout</a>
                </div>
            </li>
        </ul>
        {% endif %}
    </div>
  </nav>

在设计navbar.html时,我们添加了一些条件语句,以在用户登录时显示已登录用户的信息和注销选项。

让我们继续进行注册和登录页面。以下是注册页面的代码片段。

文件—templates/auth/signup.html

{% extends "base.html" %}

{% block body %}
<div class="container align-middle mx-auto" style="width:30%; margin-top:5%">
    <div class="card bg-light mb-3">
        <div class="card-header"><h3>Sign Up</h3></div>
        <div class="card-body">
            <form method="post">
                {{ form.hidden_tag() }}
                {% if form.errors %}
                    {% for error in form.errors.values() %}
                        <div class="alert alert-danger" role="alert">
                            {{error}}
                        </div>
                    {% endfor %}
                  {% endif %}
                <div class="form-group">
                    <label for="exampleInputEmail1">Email address</label>
                    {{ form.email(class_="form-control", id="exampleInputEmail1", placeholder="Email", maxlength=128)}}
                    <small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
                </div>
                <div class="form-group">
                    <label for="exampleInputPassword1">Password</label>
                    {{ form.password(class_="form-control", placeholder="Password") }}
                </div>
                <div class="form-group">
                    <label for="exampleInputPassword">Confirm Password</label>
                    {{ form.confirm_password(class_="form-control", placeholder="Confirm Password") }}
                </div>
                <div class="form-group">
                    {{ form.submit(class_="btn btn-primary btn-lg") }}
                    <a class="float-right" href="login">Already have account.</a>
                </div>
            </form>
        </div>
      </div>
</div>
{% endblock %}

这是注册页面的输出:

auth.signup视图的 HTTP GET请求中,这将返回一个空表单,并通过signup.html模板进行渲染。我们还添加了代码来在注册视图中接收 HTTP POST请求上的注册数据。我们使用User模型在注册过程中持久化用户数据。

这是登录模板。

文件—templates/auth/login.html

{% extends "base.html" %}

{% block body %}
<div class="container align-middle mx-auto" style="width:30%; margin-top:5%">
    <div class="card bg-light mb-3">
        <div class="card-header"><h3>Login</h3></div>
        <div class="card-body">
            <form method="post">
                {{ form.hidden_tag() }}
                {% if form.errors %}
                    <div class="has-error"><strong>Unable to login. Typo?</strong></div>
                  {% endif %}
                <div class="form-group">
                    <label for="exampleInputEmail1">Email address</label>
                    {{ form.email(class_="form-control", id="exampleInputEmail1", placeholder="Email", maxlength=128)}}
                    <small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
                </div>
                <div class="form-group">
                    <label for="exampleInputPassword1">Password</label>
                    {{ form.password(class_="form-control", id="exampleInputPassword1", placeholder="Password") }}
                </div>
                <div class="form-group">
                    {{ form.submit(class_="btn btn-primary btn-lg") }}
                    <a class="float-right" href="signup">New around here? Sign up</a>
                </div>
            </form>
        </div>
      </div>
</div>
{% endblock %}

现在,用户可以继续登录系统。对于登录,我们创建了登录表单,并通过auth.login视图进行渲染。以下是登录页面的截图:

在 HTTP POST请求中,我们使用Flask-Login扩展来处理用户登录机制,它提供了一个名为login_user的函数并执行登录过程。它创建一个会话并将user_id添加到会话中,以便在进一步的请求中记住用户,直到我们从会话中移除用户或使用auth.logout视图中提到的logout_user方法执行注销。

认证过程在这里完成,当用户登录成功并重定向到另一个页面或模板时。现在,是时候继续进行todo模块了。

Todo

Todo 程序被认为是一个简单直接的应用程序,并且在 hello world!之后广泛用于解释任何语言或框架。我们也为todo模块遵循相同的脚手架结构。

以下是todo模块的脚手架的截图:

让我们看看todo模块中每个文件的详细描述。

蓝图

Flask 引入了蓝图的概念,用于开发应用程序组件和命令模式,可以在应用程序或多个应用程序中使用。它有助于通过将根 Flask 应用程序对象集中化来理解大型应用程序。蓝图充当一个独立的 Flask 应用程序,而不创建实际的 Flask 应用程序对象,并且能够实例化应用程序对象,初始化多个扩展,并注册集合。它还提供模板过滤器、静态文件、模板和其他实用程序。

auth模块中所述,我们还将为 Todo 应用程序创建Blueprint实例。这将在app.__init__.py文件中配置,这是我们创建 Flask 应用程序实例的地方。

todo module's blueprint.

文件—todo/__init__.py

from flask import Blueprint

todo = Blueprint('todo', __name__)

from . import views 

一旦我们创建了todo模块的blueprint对象,我们就可以使用它在视图中添加路由,并将 blueprint 注册到 Flask 应用程序实例中。

app/__init__.py, which is where we are going to register blueprint:
from .auth import auth as auth_blueprint
from app.config import config

app = Flask(__name__)
app.config.from_object(config[environment])

app.register_blueprint(todo_blueprint, url_prefix='/todos')

模型

我们将使用Flask-SQLAlchemy来创建一个待办事项模型。它将与User模型建立关系,并带有一个反向引用,这样我们就可以查询与User模型相关的todo数据。

以下是待办事项模型的代码片段。

文件-todo/models.py

from datetime import datetime
from app import db
from app.config.models import BaseModel

class Todo(db.Model, BaseModel):
    __tablename__ = 'todo'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(128))
    is_completed = db.Column(db.Boolean, default=False)
    created_by = db.Column(db.String(64), db.ForeignKey('user.email'))
    user = db.relationship('User', backref=db.backref('todos', lazy=True))

    def __init__(self, title, created_by=None, created_at=None):
        self.title = title
        self.created_by = created_by
        self.created_at = created_at or datetime.utcnow()

    def __repr__(self):
        return '<{0} Todo: {1} by {2}>'.format(
            self.status, self.title, self.created_by or 'None')

    @property
    def status(self):
        return 'finished' if self.is_completed else 'open'

    def finished(self):
        self.is_completed = True
        self.finished_at = datetime.utcnow()
        self.save()

    def reopen(self):
        self.is_completed = False
        self.finished_at = None
        self.save()

    def to_dict(self):
        return {
            'title': self.title,
            'created_by': self.created_by,
            'status': self.status,
        }

在这里,我们使用基本功能和验证创建了待办事项模型。现在,我们将使用这个模型来持久化todo数据。然而,我们还需要为用户提供一个 UI 来输入todo数据并执行一些操作。

表单

我们将拥有一个简单的待办事项表单,其中包含一个带有提交按钮的文本框。它还应该包含列表视图来显示待办事项数据。

以下是待办事项表单的代码片段。

文件-todo/forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import Required, Length

class TodoForm(FlaskForm):
    title = StringField(
        'What needs to be done?', validators=[Required(), Length(1, 128)]
    )
    submit = SubmitField('Submit')

正如你所看到的,我们的待办事项表单非常简单,带有一些基本的验证。现在是时候在视图中使用这个表单来将它们渲染成 HTML 模板了。

视图

我们创建了一个待办事项蓝图的实例,并将使用这个实例在视图中创建路由。以下是视图的代码片段。

文件-todo/views.py

import json

from flask import render_template, redirect, url_for, jsonify, request
from flask_login import login_required, current_user
from app.todo import todo
from app.todo.forms import TodoForm
from app.todo.models import Todo

@todo.route('/', methods=['GET', 'POST'])
@login_required
def list():
    context = dict()
    form = TodoForm()
    if form.validate_on_submit():
        Todo(form.title.data, created_by=current_user.email).save()
        return redirect(url_for('todo.list'))
    context['form'] = form
    context['todos'] = current_user.todos
    context['items_left'] = len([todo for todo in current_user.todos if not todo.is_completed])
    return render_template('todo/list.html', **context)

@todo.route('/<todo_id>', methods=['DELETE'])
@login_required
def remove(todo_id):
    Todo.query.filter_by(id=int(todo_id)).delete()
    return jsonify({'message': 'Todo removed successfully'})

@todo.route('/<todo_id>', methods=['PATCH'])
@login_required
def update(todo_id):
    data = json.loads([k for k in request.form.keys()][0])
    todo = Todo.query.filter_by(id=int(todo_id)).scalar()
    if data.get('status'):
        todo.finished()
    else:
        todo.reopen()
    return jsonify({'message': 'Todo updated successfully'})

我们在这里定义了三个路由。在注册待办事项蓝图到 Flask 应用对象时,我们已经使用了todos作为前缀。记住这一点,我们决定使用这些路由 URL。

为了持久化待办事项数据,我们需要执行四种类型的操作,即—创建一个待办事项,列出待办事项,更新任何特定项目,和删除任何特定的待办事项。这些操作无非是标准的CRUD创建检索更新删除)操作。

创建

为了创建一个操作,我们决定将 URL 设置为/,但是加上前缀后,它将变成todos/。在 HTTP POST请求中,我们期望从用户那里得到待办事项数据,根据提交的数据,我们将使用待办事项模型创建待办事项数据,例如Todo(form.description.data, creator=current_user.email).save()

检索

current_user.todos and filter the data using list compensation. Then, we prepare the context and pass it to the render_template method to display the data in HTML.

更新

要更新待办事项数据,我们将使用 HTTP PATCH请求来访问路由todos/<todo_id>。但是,这次我们没有任何表单,需要传递数据,因此我们使用 jQuery 来进行PATCH请求的 Ajax 查询。

我们定义了一些属性和方法来标记待办事项数据是否完成,因此根据更新的数据,我们将使用这些方法来更新待办事项记录。

删除

类似于从数据库中删除待办事项记录,我们需要使用待办事项模型的查询方法,比如Todo.query.filter_by(id=int(todo_id)).delete()。正如你所看到的,路由视图非常简单。现在,让我们来看一下模板。

模板

需要做很多工作来完成待办事项的工作流。我们定义了templates/todo/list.html模板来显示待办事项表单和待办事项记录列表。在前面的部分中,我们描述了如何渲染和传递上下文数据。

以下是待办事项列表模板的代码片段。

文件-templates/todo/list.html

{% extends "base.html" %}
{% block js %}
    <script src="img/list.js')}}"></script>
{% endblock %}
{% block body %}
<div class="container align-middle mx-auto" style="width:30%; margin-top:5%">
    <div class="card mb-3">
        <div class="card-header" align="center"><h3>todo's</h3></div>
        <div class="card-body">
            <form method="post" class="form-inline">
                {{ form.hidden_tag() }}
                {% if form.errors %}
                    <div class="has-error"><strong>Invalid task. Typo?</strong></div>
                  {% endif %}
                <div class="form-group ml-3">
                    {{ form.title(class_="form-control", placeholder="What needs to be done?", maxlength=128)}}
                </div>
                <div class="form-group">
                    {{ form.submit(class_="btn btn-primary ml-2") }}
                </div>
            </form>
            <div class="badge badge-pill badge-info ml-3 mt-2">
                {{items_left}} items left
            </div>
            <ul class="list-group list-group-flush mt-3" id="todolist">
                {% for todo in todos %}
                <li class="list-group-item" id="{{todo.id}}">
                    <input type="checkbox" aria-label="Checkbox for following text input" {% if todo.is_completed %} checked {% endif %}>
                    {{todo.title}}
                    <span class="badge badge-danger badge-pill float-right">X</span>
                </li>
                {% endfor %}
            </ul>
        </div>
      </div>
</div>

<script>

</script>
{% endblock %}

我们使用上下文数据来显示待办事项表单和记录列表。有一些操作我们需要编写 jQuery 代码,比如根据复选框操作更新待办事项,以及根据删除按钮操作删除待办事项。

以下是 jQuery 代码片段。

文件-static/todo/list.js

var csrftoken = $('meta[name=csrf-token]').attr('content');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$.ajaxSetup({
    beforeSend: function(xhr, settings) {
      if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
        xhr.setRequestHeader("X-CSRFToken", csrftoken);
      }
    }
  });

$(document).ready(function(){

    // Update todo
    $('#todolist li>input[type="checkbox"]').on('click', function(e){
        var todo_id = $(this).parent().closest('li').attr('id');
        $.ajax({
            url : todo_id,
            method : 'PATCH',
            data : JSON.stringify({status: $(this).prop('checked')}),
            success : function(response){
                location.reload();
            },
            error : function(error){
                console.log(error)
            }
        })
    })

    // Remove todo
    $('#todolist li>span').on('click', function(e){
        var todo_id = $(this).parent().closest('li').attr('id');
        $.ajax({
            url : todo_id,
            method : 'DELETE',
            success : function(response){
                location.reload();
            },
            error : function(error){
                console.log(error)
            }
        })
    })
})

在进行 Ajax 请求时,我们还添加了对 CSRF 的支持。Ajax 请求非常简单直接,因为这些请求是通过前面提到的 todo 路由来服务的。以下是待办事项列表页面的截图:

现在,我们已经完成了todo模块,是时候用 Flask 应用对象配置 todo 蓝图了。

FLASK_APP

在任何 Flask 项目中,我们创建 Flask 应用程序对象,并使用FLASK_APP参数或环境变量的值引用文件路径。在我们的情况下,我们创建了一个模块化应用程序,为特定操作定义了单独的模块,但现在我们需要将所有这些模块合并到一个地方。我们已经看到了blueprint对象及其集成。在这里,我们将看到将blueprint和其他所需扩展组合的实际过程。

以下是 Flask 应用程序对象的代码片段。

文件-app/__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_wtf.csrf import CSRFProtect

from app.config import config

db = SQLAlchemy()
migrate = Migrate()
csrf = CSRFProtect()

login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
login_manager.login_message_category = "info"

def create_app(environment):
    app = Flask(__name__)
    app.config.from_object(config[environment])

    csrf.init_app(app)
    db.init_app(app)
    migrate.init_app(app, db=db)
    login_manager.init_app(app)

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')

    from .todo import todo as todo_blueprint
    app.register_blueprint(todo_blueprint, url_prefix='/todos')

    return app

在这里,我们正在配置扩展和蓝图,但是在一个名为create_app的方法下。这个方法需要一个参数来设置特定环境的配置,因此最好有这个函数,并为特定配置获取 Flask 应用程序实例。

run.py, where we will be using the create_app method.

文件-flask_todo/run.py

from app import create_app

app = create_app('dev')

在这里,我们使用了dev环境配置。您可以将此文件用作您的FLASK_APP参数,例如FLASK_APP=run.py flask run

我们已经完成了 todo 应用程序的开发,现在是时候使用 Zappa 进行部署了。

部署

我们将使用 Zappa 进行部署。要配置 Zappa,您需要安装 Zappa 并使用 AWS CLI 配置您的 AWS 凭据。一旦我们安装了 Zappa 并处理了 AWS CLI 配置,我们就可以继续部署 Todo 应用程序。

以下是zappa init命令过程的截图:

当我们运行zappa init命令时,Zappa 会自动识别框架类型并建议所需的参数。在我们的情况下,我们将app_function名称保持为run.app,因为我们是通过run.py中的create_app方法初始化flask app对象。

zappa init命令创建了zappa_settings.json文件,其中包含了所有配置的参数。您可以根据需要自由修改它。

现在,是时候使用zappa deploy <stage_name>命令执行部署过程了。最初,我们将使用zappa deploy命令。一旦我们的应用程序部署完成,我们就不能再使用zappa deploy命令了。相反,我们需要使用zappa update <stage_name>命令。

以下是zappa deploy dev命令的代码:

$ zappa deploy dev
Calling deploy for stage dev..
Creating chapter-3-dev-ZappaLambdaExecutionRole IAM Role..
Creating zappa-permissions policy on chapter-3-dev-ZappaLambdaExecutionRole IAM Role.
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter-3-dev-1529318192.zip (9.4MiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 9.87M/9.87M [00:05<00:00, 1.89MB/s]
Scheduling..
Scheduled chapter-3-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Uploading chapter-3-dev-template-1529318886.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 4.87KB/s]
Waiting for stack chapter-3-dev to create (this can take a bit)..
 50%|██████████████████████████████████████████████████████████████████████████████████████████████▌ | 2/4 [00:09<00:10, 5.29s/res]
Deploying API Gateway..
Deployment complete!: https://m974nz8zld.execute-api.ap-south-1.amazonaws.com/dev

我们已经完成了部署,并且能够访问生成的 URL 上的 Todo 应用程序,如下截图所示。

访问 URL 后的输出如下(m974nz8zld.execute-api.ap-south-1.amazonaws.com/dev/auth/signup)

我将保持flask_todo Lambda 函数处于活动状态,以便您随时尝试。我已经创建了一个 GitHub 存储库(github.com/PacktPublishing/Building-Serverless-Python-Web-Services-with-Zappa/tree/master/chapter_3),并将所有代码库推送到其中以供将来参考。

总结

在本章中,我们介绍了使用 Zappa 在服务器环境上创建基于 Flask 的应用程序并部署的工作流程。借助 Zappa,我们将应用程序移动到 AWS Lambda 并执行操作以维护部署。在部署应用程序时,我们不需要配置传统的服务器软件;相反,我们只需使用 JSON 文件来配置具有多个环境的部署。

在下一章中,我们将看到 REST API 的实现。

问题

  1. Amazon API Gateway 是什么?

  2. zappa_settings.json中的function_name的用途是什么?

第四章:使用 Zappa 构建基于 Flask 的 REST API

到目前为止,我们已经看到了如何开发基于 Flask 的应用程序,并在无服务器基础架构上部署它,我们已经创建了一个完整的 Web 应用程序以及 HTML 渲染过程,并且我们已经使用了各种 Flask 扩展以非常高效的方式构建了应用程序。

在本章中,我们将开发基于 Flask 的 RESTful API。这将涵盖使用 Flask 实现 REST API 并使用 Zappa 部署的 REST API。在第一章中,无服务器的亚马逊网络服务,我们看到了集成 AWS Lambda 和 API Gateway 的手动过程,所以现在我们将使用 Zappa 以自动化方式部署 REST API。Zappa 将通过配置代理设置来处理 Flask REST API 与 API Gateway 的集成,以传递请求以调用 Lambda 函数。

让我们继续我们的旅程,开发基于无服务器架构的 REST API。

在本章中,我们将涵盖以下主题:

  • 安装和配置 Flask

  • 设计 REST API

  • 集成 Zappa

  • 使用 Zappa 构建、测试和部署 REST API

技术要求

在开始实际实现之前,我们需要一些先决条件来创建基于 Flask 的 REST API:

  • Ubuntu 16.04 LTS

  • Python 3.6

  • 虚拟环境

  • Flask 0.12.2

  • Flask-JWT 0.3.2

  • Flask-SQLAlchemy 2.3.2

  • Flask-Migrate 2.1.1

  • Flask-RESTful 0.3.6

  • Zappa 0.45.1

  • Flask

  • Flask 扩展

安装和配置 Flask

我们将开发一个基于 Flask 的 REST API,将其部署为 AWS Lambda 上的无服务器。因此,在这里,安装和配置 Flask 将在虚拟环境中进行。

我们将创建一个虚拟环境,并使其能够安装所有必需的软件包。可以使用以下命令完成:

virtualenv .env -p python3.6
source .env/bin/activate

现在,我们将列出requirements.txt文件中的所有必需软件包,并一次性安装所有软件包。以下描述了requirements.txt文件的内容:

Flask==0.12.2
Flask-JWT==0.3.2
Flask-SQLAlchemy==2.3.2
Flask-Migrate==2.1.1
flask-restful==0.3.6
zappa==0.45.1

现在,我们可以使用以下命令安装所有这些软件包:

$ pip install -r requirements.txt

这是将在虚拟环境中安装的所有软件包。现在,让我们在下一节详细解释这些软件包。

Flask 扩展

Flask 有许多可用的扩展,可以增强任何所需功能的能力。在我们的应用程序中,我们将使用多个扩展,如前一节中所述。这些扩展遵循一个通用模式,以便我们可以将它们与 Flask 应用程序对象集成。

我们将设计一个基于 Flask 的 REST API 应用程序,该应用程序将通过遵循 REST API 通信标准和验证,在 Todo 模型上具有基本的身份验证、授权和 CRUD 操作。

让我们在接下来的章节中看看这些扩展的用法。

Flask-JWT

Flask-JWT 扩展在 Flask 环境中启用了JWTJSON Web Token)功能。在设计 REST API 时,JWT 令牌对于验证和授权 API 访问起着重要作用。我们将在下一节中详细描述 JWT。

学习 JWT

JWT代表JSON Web Token。这是一种标准模式,用于实现 REST API 接口的安全性和真实性访问。JWT 令牌是服务器应用程序发出的数据的编码形式,用于验证客户端访问。客户端需要在 HTTP 请求中添加 JWT 令牌作为授权标头。

我们将使用 JWT 令牌来验证 REST API 的访问。如果您需要详细了解 JWT 机制,我建议阅读jwt.io/introduction/上的 JWT 文档。

Flask-RESTful

Flask-RESTful 扩展旨在使用 Flask 框架实现 REST API。该扩展遵循标准的 REST API 实现模式,并提供了一种实现 REST API 的简单方法。在实现 REST API 之前,您必须对 REST API 标准有基本的了解,因此让我们来看看 REST API 的基础知识。

开始 REST

REST代表REpresentational State Transfer.它是一个明确定义的标准,用于实现服务器-客户端通信以持久化数据。REST 遵循JSONJavaScript 对象表示)数据表示格式来交换数据。

REST 在 HTTP 方法上定义了一些动词,用于执行 CRUD 操作,例如:

  • GET:检索记录列表和根 URL 中带有后缀 ID 参数的特定记录,还返回带有 200 状态代码的响应

  • POST:在服务器上创建记录,并返回带有 201 状态代码的响应

  • PUT:更新服务器上的所有记录字段,并返回带有 200 状态代码的响应

  • PATCH:更新服务器上记录集中的特定字段,并返回带有 200 状态代码的响应

  • DELETE:通过 URL 中的记录特定 ID 的帮助删除整个记录集,并返回带有 204 状态代码的响应

现在是时候看一些实际工作了。让我们继续下一节。

设计 REST API

我们将设计一个 REST API,用于对我们的 todos 模型执行 CRUD 操作。我们的应用程序将具有基本的身份验证和授权工作流,以保护 REST API 端点。

以下是我们应用程序的脚手架:

__init__.py, where we configured the Flask application object with extensions and the config object.

文件—app/__init__.py:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_jwt import JWT, jwt_required, current_identity

from app.config import config

db = SQLAlchemy()
migrate = Migrate()

def create_app(environment):
    app = Flask(__name__)
    app.config.from_object(config[environment])

    db.init_app(app)
    migrate.init_app(app, db=db)

    from .auth.models import User

    def authenticate(email, password):
        data = request.json
        user = User.query.filter_by(email=data['email']).first()
        if user is not None and user.verify_password(data['password']):
            return user

    def identity(payload):
        user_id = payload['identity']
        return User.query.filter_by(id=user_id).first()

    jwt = JWT(app, authenticate, identity)

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')

    from .todo import todo as todo_blueprint
    app.register_blueprint(todo_blueprint)

    return app

我们配置了 Flask 扩展,如 Flask-SQLAlchemy 和 Flask-Migration,这些都很简单。Flask-JWT 集成需要更多的工作,因为我们需要定义authenticateidentity方法,并在初始化 JWT 对象时将它们用作参数。这些方法负责对用户进行身份验证和识别用户。

除了扩展集成,我们将创建authtodoapps作为 Flask 蓝图对象,并使用register_blueprint方法将它们注册到 Flask 应用程序对象中。

让我们详细描述每个包及其用途。

配置应用程序设置

config包中,我们定义了应用程序级别的配置,根据定义的环境进行隔离。以下是config.py文件的内容。

文件—config/config.py:

import os
from shutil import copyfile

BASE_DIR = os.path.dirname(os.path.dirname(__file__))

def get_sqlite_uri(db_name):
    src = os.path.join(BASE_DIR, db_name)
    dst = "/tmp/%s" % db_name
    copyfile(src, dst)
    return 'sqlite:///%s' % dst

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(24)
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True
    SQLALCHEMY_RECORD_QUERIES = True
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config):
    DEBUG = True
    BUNDLE_ERRORS = True
    SQLALCHEMY_DATABASE_URI = get_sqlite_uri('todo-dev.db')

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = get_sqlite_uri('todo-prod.db')

config = {
    'dev': DevelopmentConfig,
    'prod': ProductionConfig,
}

config文件公开了config对象,其中包含根据您的环境不同的配置对象。类似地,您可以根据需要添加更多环境。

get_sqlite_uri方法被定义为将db文件设置在tmp目录中,因为 AWS Lambda 要求在执行时将 SQLite.db文件保存在内存中。

BaseModel, which was inspired by Django's standard pattern to perform save, update, and delete operations. We can add more generic features if required.

文件—config/models.py:


from app import db

class BaseModel:
    """
    Base Model with common operations.
    """

    def delete(self):
        db.session.delete(self)
        db.session.commit()

    def save(self):
        db.session.add(self)
        db.session.commit()
        return self

在这里,我们将db会话操作组合在一起,以执行特定的事务,如保存、更新和删除。这将帮助我们扩展模型类的功能。

实施认证

认证是保护 REST API 免受未经授权访问的重要功能。因此,为了实现认证层,我们将使用 JWT 机制。在这里,我们将设计两个 REST API,用于注册用户和登录访问。

User model.

文件—auth/models.py:

import re
from datetime import datetime

from app.config.models import BaseModel
from sqlalchemy.orm import synonym
from werkzeug.security import generate_password_hash, check_password_hash
from app import db

class User(BaseModel, db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    _email = db.Column('email', db.String(64), unique=True)
    password_hash = db.Column(db.String(128))

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)

    def __repr__(self):
        return '<User {0}>'.format(self.email)

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        if not len(email) <= 64 or not bool(re.match(r'^\S+@\S+\.\S+$', email)):
            raise ValueError('{} is not a valid email address'.format(email))
        self._email = email

    email = synonym('_email', descriptor=email)

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        if not bool(password):
            raise ValueError('no password given')

        hashed_password = generate_password_hash(password)
        if not len(hashed_password) <= 128:
            raise ValueError('not a valid password, hash is too long')
        self.password_hash = hashed_password

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    def to_dict(self):
        return {
            'email': self.email
        }

这是一个基本的User模型,只有两个字段,即emailpassword。现在,我们将设计一个注册 API 和一个登录 API。注册 API 将只接受两个参数,电子邮件和密码,并将在数据库中创建一个用户记录。登录 API 将用于验证用户的凭据,并返回一个 JWT 令牌,该令牌将与其他 API 一起作为授权标头使用。

让我们创建注册和登录 API。以下是资源文件的代码片段,其中包括 API 实现逻辑的内容。

文件 - auth/resources.py

from flask import request, jsonify
from flask_restful import Resource, reqparse, abort
from flask_jwt import current_app
from app.auth.models import User

def generate_token(user):
    """ Currently this is workaround
    since the latest version that already has this function
    is not published on PyPI yet and we don't want
    to install the package directly from GitHub.
    See: https://github.com/mattupstate/flask-jwt/blob/9f4f3bc8dce9da5dd8a567dfada0854e0cf656ae/flask_jwt/__init__.py#L145
    """
    jwt = current_app.extensions['jwt']
    token = jwt.jwt_encode_callback(user)
    return token

class SignUpResource(Resource):
    parser = reqparse.RequestParser(bundle_errors=True)
    parser.add_argument('email', type=str, required=True)
    parser.add_argument('password', type=str, required=True)

    def post(self):
        args = self.parser.parse_args()
        if not User.query.filter_by(email=args['email']).scalar():
            User(
                email = args['email'],
                password = args['password']
            ).save()
            return {'message': 'Sign up successfully'}
        abort(400, message='Email already exists.')

class LoginResource(Resource):
    parser = reqparse.RequestParser(bundle_errors=True)
    parser.add_argument('email', type=str, required=True)
    parser.add_argument('password', type=str, required=True)

    def post(self):
        args = self.parser.parse_args()
        user = User.query.filter_by(email=args['email']).first()
        if user is not None and user.verify_password(args['password']):
            token = generate_token(user)
            return jsonify({'token': token.decode("utf-8")})
        abort(400, message='Invalid credentials')

Flask-RESTful 提供了一个Resource类,用于定义 API 资源。它遵循 REST 标准,并提供了一个简单的方法来创建 API。由于我们将在 HTTP 的大多数request方法上使用注册 API,我们创建了一个post方法。同样,我们设计了登录 API,我们在那里验证用户的凭据并返回一个令牌。

我们必须返回自定义方法来生成令牌,因为在撰写本文时,Flask-JWT PyPI仓库尚未发布更新版本,尽管这个功能已经添加到 GitHub 仓库中。

auth/__init__.py file.

文件 - auth/__init__.py

from flask import Blueprint
from flask_restful import Api
from .resources import SignUpResource, LoginResource

auth = Blueprint('auth', __name__)
auth_api = Api(auth, catch_all_404s=True)

auth_api.add_resource(SignUpResource, '/signup', endpoint='signup')
auth_api.add_resource(LoginResource, '/login', endpoint='login')

在这里,我们创建了Blueprint对象并对其进行了配置。Flask-RESTful 提供了一个API类,使用这个类,我们注册了我们的注册和登录资源。就是这样。现在,我们可以用 JSON 数据访问注册和登录 URL 来执行操作。在部署过程之后,我们将对这些 REST API 进行完整演示。

实现 todo API

让我们开始 todo API 的实现。我们需要一个 todo REST API 端点来执行 CRUD 操作。根据 REST 标准,只会有一个端点 URL,比如/todos/<todo_id>/。这个端点将用于将 todo 数据持久化到数据库中。我们需要有一个 Todo 模型来持久化数据。以下是 Todo 模型的代码片段。

文件 - todo/models.py

from datetime import datetime
from app import db
from app.config.models import BaseModel

class Todo(db.Model, BaseModel):
    __tablename__ = 'todo'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(128))
    is_completed = db.Column(db.Boolean, default=False)
    created_by = db.Column(db.String(64), db.ForeignKey('user.email'))
    user = db.relationship('User', backref=db.backref('todos', lazy=True))

    def __init__(self, title, created_by=None, created_at=None):
        self.title = title
        self.created_by = created_by

    def __repr__(self):
        return '<{0} Todo: {1} by {2}>'.format(
            self.status, self.title, self.created_by or 'None')

    @property
    def status(self):
        return 'completed' if self.is_completed else 'open'

    def completed(self):
        self.is_completed = True
        self.save()

    def reopen(self):
        self.is_completed = False
        self.save()

    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'created_by': self.created_by,
            'status': self.status,
        }
resources.py, which contains the todo's REST API.

文件 - todo/resources.py

from flask import request
from flask_restful import Resource, reqparse
from flask_jwt import current_identity, jwt_required

from .models import Todo

class TodoResource(Resource):

    decorators = [jwt_required()]

    def post(self):
        parser = reqparse.RequestParser(bundle_errors=True)
        parser.add_argument('title', type=str, required=True)

        args = parser.parse_args(strict=True)
        todo = Todo(args['title'], created_by=current_identity.email).save()
        return todo.to_dict(), 201

    def get(self, todo_id=None):
        if todo_id:
            todos = Todo.query.filter_by(id=todo_id, created_by=current_identity.email)
        else:
            todos = Todo.query.filter_by(created_by=current_identity.email)
        return [todo.to_dict() for todo in todos]

    def patch(self, todo_id=None):
        parser = reqparse.RequestParser(bundle_errors=True)
        parser.add_argument(
            'status',
            choices=('open', 'completed'),
            help='Bad choice: {error_msg}. Valid choices are \'open\' or \'completed\'.',
            required=True)

        if not todo_id:
            return {'error': 'method not allowed'}, 405
        args = parser.parse_args(strict=True)
        todo = Todo.query.filter_by(id=todo_id, created_by=current_identity.email).scalar()
        if args['status'] == "open":
            todo.reopen()
        elif args['status'] == 'completed':
            todo.completed()
        else:
            return {'error':'Invalid data!'}, 400
        return todo.to_dict(), 202

    def delete(self, todo_id=None):
        if not todo_id:
            return {'error': 'method not allowed'}, 405
        Todo.query.filter_by(id=int(todo_id), created_by=current_identity.email).delete()
        return {}, 204

在这里,我们定义了TodoResource类,它将处理GETPOSTPUTDELETE的 HTTP 请求。根据请求类型,我们执行 CRUD 操作。我们还使用reqparse来定义从 HTTP 请求中所需数据的验证。

为了保护TodoResource,我们在TodoResource类的装饰器列表中添加了jwt_required方法,这将应用于所有相关方法。现在,TodoResource API 只能在有效的授权头部下使用,否则将会响应未经授权的访问错误。

我们将在接下来的章节中看到这个完整的工作过程。

使用 Zappa 构建、测试和部署 REST API

我们已经完成了开发,现在是时候将应用程序作为无服务器部署在 AWS Lambda 上了。我们已经在前一章中描述了配置 Zappa 及其相关配置的先决条件,所以这里我假设你已经配置了 Zappa 以及 AWS 配置。

配置 Zappa

一旦你配置了 Zappa,你可以为你的项目初始化 Zappa。你需要运行zappa init命令,并按照 CLI 问卷的指引来配置你的项目与 Zappa。我按照 Zappa 建议的默认配置设置进行了配置。zappa init命令将生成zappa_settings.json文件,我们可以根据需要自由修改这个文件。

zappa_settings.json file.

文件 - zappa_settings.json

{
    "dev": {
        "app_function": "run.app",
        "aws_region": "ap-south-1",
        "profile_name": "default",
        "project_name": "chapter-4",
        "runtime": "python3.6",
        "s3_bucket": "zappa-5xvirta98"
    }
}

Zappa 维护这个 JSON 文件以执行部署过程。现在,我们将继续部署应用程序。

使用 Zappa 启动部署

一旦你完成了 Zappa 的初始化,就该部署应用程序了。Zappa 提供了一个zappa deploy命令来部署应用程序。这个命令将执行部署过程,它将创建部署包作为 ZIP 文件,将其推送到 AWS S3,并配置 AWS Lambda 与 API Gateway。我们在第一章中详细描述了完整的部署过程。

一旦我们用zappa deploy dev命令运行这个,我们的应用程序将作为无服务器应用程序托管在 AWS Lambda 上。如果你想重新部署相同的应用程序,那么你需要运行zappa update dev命令,这将更新现有的应用程序。

让我们在下一节中看一下部署的应用程序演示。

演示部署的应用程序

Zappa 为部署的应用程序生成一个随机 URL,并且在每次新部署时都会生成 URL。但是,如果您只是更新部署,则不会更改 URL。这是我们从 Zappa 部署过程中获得的 URL:jrzlw1zpdi.execute-api.ap-south-1.amazonaws.com/dev/。我们已经使用一些端点编写了 auth 和 todo API,因此您在基本 URL 上看不到任何内容。我们将使用在资源中定义的 API 端点。

注册 API

我们设计了带有端点/auth/signup的注册 API,它期望两个参数—emailpassword。此端点负责在数据库中创建用户记录。一旦我们获得成功的响应,我们可以使用相同的用户凭据执行登录并访问其他 API。

以下是注册 API 的截图:

在这里,我们使用高级 REST 客户端应用程序测试 API。如您所见,我们正在使用注册 API 创建用户记录。注册 API 以状态码 200 进行响应。

登录 API

现在,我们在数据库中有一个用户记录,我们可以使用它来执行登录操作。登录 API 负责验证用户的凭据并返回 JWT 令牌。此 JWT 令牌将用于授权 todos API。以下是通过 REST 客户端使用的登录 API 的截图:

在这里,您可以看到登录 API 的执行,因为我们获得了将用于授权访问待办事项 API 的 JWT 令牌。

待办事项 API

现在我们通过登录 API 获得了 JWT 令牌,让我们执行待办事项 API。然而,在这里,我们将看到待办事项 API 的不同场景。我们的待办事项 API 有一个名为todos/<todo_id>的端点。

没有授权的待办事项 API

让我们尝试在不提供授权标头的情况下使用待办事项 API:

如您所见,我们从应用程序得到了未经授权的错误。现在,我们将提供带有 JWT 令牌的授权标头。

带有授权标头的待办事项 API

我们将使用登录 API 返回的 JWT 令牌,并设置授权标头。授权标头的值将是JWT <token>。现在,让我们执行带有 CRUD 操作的 API。

GET请求如下所示:

在这里,我们得到了数据库中所有待办事项记录的列表。由于我们设置了授权标头,我们获得了访问权限。

POST请求如下所示:

在这里,我们创建了一个新的待办事项记录,并获得了状态码为201的响应。现在,使用基本 URL,我们可以执行GETPOST请求,但是,要执行对特定记录的GETPUTDELETE功能,我们需要在 URL 中提到todo_id

没有有效负载数据的POST请求如下所示:

在这里,由于我们没有提供任何有效负载,我们得到了验证错误。我们使用flask_restful库的reqparse模块来处理此验证。

带有待办事项 ID 的GET请求如下所示:

您可以看到我们在 URL 中使用了待办事项 ID 来查看特定记录集。

PATCH请求如下所示:

在这里,我们更新了待办事项的状态,并将待办事项记录标记为已完成。

带有无效数据的PATCH请求如下所示:

在这里,由于我们使用reqparse模块定义了必需的选项,我们得到了验证错误,如下所示:

parser = reqparse.RequestParser(bundle_errors=True)
        parser.add_argument(
            'status',
            choices=('open', 'completed'),
            help='Bad choice: {error_msg}. Valid choices are \'open\' or \'completed\'.',
            required=True)

DELETE请求如下所示:

最后,我们使用 HTTP DELETE请求删除了记录。就是这样!我们已经完成了 REST API 的实现。

摘要

在本章中,我们学习了如何创建基于 Flask 的 REST API,并使用一些扩展进行配置。在 Flask-JWT 扩展的帮助下,我们实现了安全性。Flask-RESTful 扩展提供了一个简单的接口来设计 REST API。最后,我们配置了 Zappa 来在无服务器环境中部署应用程序。

在下一章中,我们将看到 Django 应用程序开发作为 AWS Lambda 上的无服务器应用程序。敬请关注。

问题

  1. 我们为什么需要 JWT 实现?

  2. Zappa 设置文件中的function_name参数是什么?

第五章:使用 Zappa 构建 Django 应用程序

在本章中,我们将创建一个基于 Django 的图库应用程序,用户可以创建相册并上传图片。在 Django 中工作时,为静态和媒体内容提供服务是非常有趣和具有挑战性的。通常,开发人员将图像存储在文件存储中,并通过 URL 服务器提供。在这里,我们将在 AWS S3 中存储图像,并通过 AWS CloudFront 服务提供的 CDN 网络进行服务。

本章我们将涵盖的主题包括以下内容:

  • 安装和配置 Django

  • 设计图库应用程序

  • 通过 AWS CloudFront CDN 提供静态和媒体文件

  • 设置静态和媒体文件

  • 集成 Zappa

  • 使用 Zappa 构建、测试和部署 Django 应用程序

  • Django 管理命令

技术要求

在继续之前,让我们满足本章所需的一些先决条件。我们将开发一个基于 Django 的无服务器应用程序,因此我们需要满足以下用于开发此应用程序的要求:

  • Ubuntu 16.04/Mac/Windows

  • Pipenv 工具

  • Django

  • Django 存储

  • Django Imagekit

  • Boto3

  • Zappa

这些软件包是本章所需的软件包,我们将使用 pipenv 工具安装和配置这些软件包。现在我们将详细探讨配置。

安装和配置 Django

配置任何 Python 项目都需要遵循维护必要软件包版本的标准。许多开发人员喜欢维护requriements.txt文件,这有助于他们保持应用程序的稳定性。requirements.txt中特定软件包的任何版本升级可能会破坏整个应用程序。这就是开发人员严格遵循此标准以维护其应用程序的稳定版本的原因。

设置虚拟环境

我一直在遵循传统模式,直到我遇到了一个非常酷的工具,改变了我对维护requirements.txt文件的传统方法。现在你不再需要requirements.txt了。它叫做pipenv;我喜欢使用它。

Pipenv 是一个受多种不同语言的包管理工具启发的 Python 包管理工具。Pipenv 是 Python.org 官方推荐的(www.python.org/)。这个工具赋予了管理 Python 包的标准。

安装 pipenv

您可以从任何地方初始化虚拟环境,并且它将跟踪每个软件包的安装。

首先,我们需要在系统级安装pipenv。因此,如果您使用的是 macOS,则可以像这样使用 Homebrew 安装pipenv

$ brew install pipenv

如果您使用的是 Ubuntu 17.10,则可以简单地添加 PPA 存储库并使用apt命令进行安装,如下所示:

$ sudo apt install software-properties-common python-software-properties
$ sudo add-apt-repository ppa:pypa/ppa
$ sudo apt update
$ sudo apt install pipenv

您可以简单地在系统级别通过pip安装它,而不是从活动虚拟环境中使用pip。看一下这行代码:

pip install pipenv

系统级安装将是在不使用任何虚拟环境的情况下进行的安装。它安装在系统的bin目录中,并且应该可以从终端控制台执行。

现在,您可以通过在终端控制台上执行pipenv命令来查看有关pipenv命令的详细信息:

在这里,您可以看到有几个可用的命令,提供了一种非常灵活的方式来处理虚拟环境。

配置和安装软件包

现在,我们将为我们的项目创建一个虚拟环境并安装所需的软件包。

以下屏幕截图提到了虚拟环境创建过程:

如您从前面的屏幕截图中所见,我们使用以下命令创建了一个虚拟环境:

$ pipenv --python python3.6

我们明确指出了所需的 Python 版本;你也可以指定任何 Python 版本。如果你着急,只想用 Python 版本 2 或 3 初始化,那么你可以运行以下命令:

$ pipenv --two

你也可以使用这个:

$ pipenv --three
Pipfile:
[[source]]

url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[dev-packages]

[packages]

[requires]

python_version = "3.6"

它有不同的部分来管理所有的包。现在你可以使用以下命令安装任何包:

 pipenv install <package-name>

由于我们将使用 Django 框架,我们将使用 pipenv 来安装 Django,如下图所示:

一旦安装了任何包,pipenv就会创建一个Pipfile.lock文件。Pipfile.lock文件维护了每个安装包的提交哈希和依赖关系。

现在,如果你想激活虚拟环境,不用担心。你可以把一切都交给pipenvpipenv提供了一个名为pipenv shell的命令,它在内部调用虚拟环境的activate命令。现在,你将使用激活的虚拟环境 shell。

不必在 shell 中或激活虚拟环境中,你可以使用命令pipenv run <command as an argument>在虚拟环境下执行任何命令,例如:

 pipenv run python manage.py runserver

这真的很有趣,不是吗?

安装所有所需的包后,Pipfile将如下所示:

文件—Pipfile:

[[source]]

url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[dev-packages]

[packages]

django = "*"
pylint = "*"
pillow = "*"
zappa = "*"
django-storages = "*"
"boto3" = "*"
boto = "*"
django-imagekit = "*"

[requires]

python_version = "3.6"

现在,我们已经完成了所需包的配置和安装。

让我们继续下一节,我们将创建一个基于 Django 的图库应用。

设计图库应用

一旦我们完成配置,就可以开始实现应用。ImageGallery应用将是直接的——用户可以创建一个新的相册记录,并一次上传多张图片。一旦相册创建完成,我们将在列表视图中显示所有现有的相册记录,以及关联的缩略图。

让我们根据我们的需求来看看实现阶段。

设计概述

我将基于 Django 创建一个图库应用。我们将使用 Django admin 来实现 UI。Django admin 有非常漂亮的 UI/UX 设计。因此,我们将创建一些模型,比如一个PhotoAlbum模型,它将与Photo模型有一对多的关系。

然后我们只需在 Django admin 面板中注册这些模型。一旦我们完成了 admin 配置,我们将配置静态和媒体设置,将动态图片上传到 Amazon S3 存储桶,并通过 CloudFront CDN 网络提供这些静态文件。

让我们仔细看看实现。

初始化项目

一旦你配置了pipenv,你需要使用命令pipenv shell启用虚拟环境。假设你在pipenv shell 中,这只是一个激活的虚拟环境。一旦启用虚拟环境,你就可以访问已安装的包。因此,我们将通过执行以下命令创建 Django 项目框架:

django-admin.py startproject <project_name>

以下是项目创建过程的屏幕截图:

我已经创建了项目和一个应用。从之前的截图中,你可以看到项目和应用文件。

默认情况下,Django 在根urls.py文件中启用了 admin 面板。因此,我们不需要再次配置它。

现在让我们进入下一节的模型创建过程。

实现模型

我们将创建两个模型——PhotoAlbumPhoto模型,它们之间有一对多的关系。以下是gallery/models.py文件的代码片段:

文件—gallery/models.py:

from django.db import models
from django.utils.translation import gettext as _
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill

# Create your models here.

def upload_path(instance, filename):
    return '{}/{}'.format(instance.album.name, filename)

class PhotoAlbum(models.Model):
    name = models.CharField(_('album name'), max_length=50)
    created_at = models.DateTimeField(auto_now_add=True, auto_now=False)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'photo_album'
        verbose_name = 'photo album'
        verbose_name_plural = 'photo albums'

    def __str__(self):
        return self.name

class Photo(models.Model):
    album = models.ForeignKey(PhotoAlbum, related_name='photos', on_delete=models.CASCADE)
    image = models.ImageField(_('image'), upload_to=upload_path)
    image_thumbnail = ImageSpecField(source='image',
                                      processors=[ResizeToFill(100, 50)],
                                      format='JPEG',
                                      options={'quality': 60})
    created_at = models.DateTimeField(auto_now_add=True, auto_now=False)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'photo'
        verbose_name = 'photo'
        verbose_name_plural = 'photos'

    def __str__(self):
        return self.image.name.split('/')[1]

按计划,我已经创建了两个模型,以及它们的关系。在这里,PhotoAlbum很直接,因为它充当父类。Photo模型更有趣,因为我们将通过它存储图片。

Photo模型中,我正在使用django-imagekitgithub.com/matthewwithanm/django-imagekit)库来创建和存储原始上传图像的缩略图。这非常有趣,因为它有许多功能可以让我们根据需要处理图像。我的意图是创建上传图像的缩略图;因此,我相应地进行了配置。

一旦您完成模型创建,您将需要运行makemigrations和迁移命令来创建实际的数据库表。查看以下截图,以了解makemigrations命令的过程:

一旦我们运行makemigrations命令,就可以准备在管理面板中配置这些模型。让我们继续进行下一节关于配置管理面板的部分。

与管理面板集成

将模型与 Django 管理面板集成需要在根urls.py文件中启用管理 URL 配置。让我们来看一下代码:

文件—imageGalleryProject/urls.py

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]
admin.py file:

文件—gallery/admin.py

from django.contrib import admin
from django.utils.html import mark_safe
from gallery.models import PhotoAlbum, Photo
# Register your models here.

class PhotoAdminInline(admin.TabularInline):
    model = Photo
    extra = 1
    fields = ( 'image', 'image_tag', )
    readonly_fields = ('image_tag',)

    def image_tag(self, instance):
        if instance.image_thumbnail.name:
            return mark_safe('<img src="img/%s" />' % instance.image_thumbnail.url)
        return ''
    image_tag.short_description = 'Image Thumbnail'

class PhotoAlbumAdmin(admin.ModelAdmin):
    inlines = [PhotoAdminInline]

admin.site.register(PhotoAlbum, PhotoAlbumAdmin)

在这里,我们将Photo模型配置为TabularInline,这样我们就可以在一个相册下添加多张照片或图片。在将应用程序部署到 AWS Lambda 后,我们将进行完整的工作流程演示。

此时,您可以在本地计算机上运行应用程序并存储图像。但是以后,我们希望部署在 AWS Lambda 上,然后将图像存储在 Amazon S3 存储桶中,并通过 Amazon CloudFront CDN 网络提供服务。

应用程序演示

我们已经将模型配置到管理面板中。现在,我们将使用python manage.py runserver命令来运行 Django 的本地服务器。它将在http://locahost:8000 URL上启动 Django 服务器。

以下是应用程序的截图:

如前面的截图所示,我们正在创建一个相册。我们定义了一对多的关系,并使用TabularInline在创建相册时接受多张照片。看一下这个截图:

添加过程完成后,将出现列表页面。现在,您可以选择新创建的相册来查看或编辑现有的详细信息。看一下这个截图:

在这里,您可以检查先前上传的图像是否显示为缩略图。我们使用了django-imagekit库来配置缩略图图像处理。

现在,我们将在下一节中看到配置 Amazon CloudFront CDN 所需的过程,并将其与我们的应用程序集成。

配置 Amazon CloudFront CDN

Amazon CloudFront 是更受欢迎的服务之一。它提供通过 CDN 网络提供静态文件的功能,有助于以更高效的方式分发静态内容,从而提高性能并降低延迟。

配置 Amazon CloudFront 时,我们通过 AWS 用户控制台创建 CloudFront 分发。

创建 CloudFront 分发

假设您有有效的 AWS 帐户,您可以使用您的凭据登录 AWS Web 控制台。从服务下拉菜单中选择 CloudFront 服务,然后单击“创建分发”按钮,如下截图所示:

在创建分发时,Amazon 提供了两种不同的方法,即 Web 和 RTMP。Web 方法用于需要通过 CDN 网络提供的静态内容,当所有静态文件都驻留在 Amazon S3 存储桶中时使用。RTMP 方法用于分发流媒体文件,允许用户在下载完成之前播放文件。

在我们的情况下,我们将选择 Web 方法,因为我们希望分发静态文件。您可以按照以下截图中显示的方法进行选择:

选择 Web 方法后,将打开创建分发表单页面。在此页面上,我们将选择所需的字段来配置分发。看一下这个截图:

成功创建云分发后,我们将把分发与我们的 Django 应用集成。

让我们继续下一节,在那里我们将配置应用中的静态和媒体文件。

设置静态和媒体文件

在 Django 中配置静态和动态文件是必不可少的。我们如何配置和提供静态和媒体文件会影响应用程序的整体性能。因此,应该以优化的方式来配置静态和媒体文件。让我们对此进行详细讨论。

标准配置

Django 有一个标准模式来配置静态和媒体文件。静态和媒体是两个不同的问题,其中静态文件指固定内容,如 HTML、JS、CSS 和图像。Django 在settings.py中定义了一些与静态文件相关的配置,并在urls.py中配置了 URL。媒体文件指通过上传动态处理的任何文件。Django 有一个非常好的机制来配置和管理静态 HTML、JS、CSS 和图像文件。

通常,默认的 Django 静态文件配置假定您将在静态目录下与代码库一起拥有静态文件,但在我们的情况下,我们希望将所有静态内容放在 Amazon S3 存储桶下,并通过 Amazon CloudFront 分发进行提供。

django-storage

我们将使用django-storagedjango-storages.readthedocs.io/en/latest/),这是一个第三方插件,用于实现自定义存储后端。借助 Django 存储,我们将设置静态和媒体配置。

以下是设置自定义存储静态和媒体文件所需的代码片段:

文件—gallery/utils.py

from django.conf import settings
from storages.backends.s3boto import S3BotoStorage

class StaticStorage(S3BotoStorage):
    location = settings.STATICFILES_LOCATION

    @property
    def connection(self):
        if self._connection is None:
            self._connection = self.connection_class(
                self.access_key, self.secret_key,
                calling_format=self.calling_format, host='s3-ap-south-1.amazonaws.com')
        return self._connection

class MediaStorage(S3BotoStorage):
    location = settings.MEDIAFILES_LOCATION

    @property
    def connection(self):
        if self._connection is None:
            self._connection = self.connection_class(
                self.access_key, self.secret_key,
                calling_format=self.calling_format, host='s3-ap-south-1.amazonaws.com')
        return self._connection

现在我们将在settings.py文件中配置这两个自定义存储类,如下所示:

文件—imageGalleryProject/settings.py

AWS_HEADERS = {
    'Expires': 'Thu, 31 Dec 2099 20:00:00 GMT',
    'Cache-Control': 'max-age=94608000',
}

AWS_STORAGE_BUCKET_NAME = 'chapter-5'
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_CLOUDFRONT_DOMAIN = 'dl76lqo8jmttq.cloudfront.net'

MEDIAFILES_LOCATION = 'media'
MEDIA_ROOT = '/%s/' % MEDIAFILES_LOCATION
MEDIA_URL = '/%s/%s/' % (AWS_CLOUDFRONT_DOMAIN, MEDIAFILES_LOCATION)
DEFAULT_FILE_STORAGE = 'gallery.utils.MediaStorage'

STATICFILES_LOCATION = 'static'
STATIC_ROOT = '/%s/' % STATICFILES_LOCATION
STATIC_URL = '/%s/%s/' % (AWS_CLOUDFRONT_DOMAIN, STATICFILES_LOCATION)
STATICFILES_STORAGE = 'gallery.utils.StaticStorage'

这些是您需要放入settings.py中的设置,现在是时候配置urls.py了。我建议您更新根urls.py,如下所示:

文件—imageGalleryProject/urls.py

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ... the rest of your URLconf goes here ...
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
  + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

一旦您配置了 URL,那么您就准备好了。要验证配置,您可以运行collectstatic命令,将所有静态文件收集到配置的位置:

$ python manage.py collectstatic

此命令将检索属于所述INSTALL_APPS的所有静态文件,并将它们上传到STATIC_ROOT。现在,当您上传任何文件时,它将被上传到 Amazon S3,并通过 Amazon CloudFront 提供。

现在是时候配置 Zappa 并进行部署了。

使用 Zappa 构建、测试和部署 Django 应用

Zappa 配置很简单。Zappa 包也可以在 pip 仓库中找到。但我们将使用 pipenv 来安装它,这可以帮助我们跟踪版本管理。以下是安装 Zappa 所需的命令:

$ pipenv install zappa

安装 Zappa 后,您需要使用zappa init命令初始化 Zappa。此命令将提示一个 shell 调查问卷,以配置 Zappa 所需的基本信息。让我们看看下一节,我们将讨论 Zappa 的基本配置。

配置 Zappa

zappa_settings.json file:
{
    "dev": {
        "aws_region": "ap-south-1",
        "django_settings": "imageGalleryProject.settings",
        "profile_name": "default",
        "project_name": "imagegallerypro",
        "runtime": "python3.6",
        "s3_bucket": "chapter-5",
        "remote_env": "s3://important-credentials-bucket/environments.json"
    }
}

在这里,我们根据要求定义了配置。由于密钥定义了每个配置,我们可以看到它的用法。考虑以下内容:

  • aws_region:Lambda 将上传的 AWS 区域

  • django_settings:Django 设置文件的导入路径

  • profile_name:在~/.aws/credentials文件中定义的 AWS CLI 配置文件

  • project_name:上传 Lambda 函数的项目名称

  • runtime:Python 运行时解释器

  • s3_bucket:创建一个 Amazon S3 存储桶并上传部署包

  • remote_env:设置 Amazon S3 位置上传的 JSON 文件中提到的所有键值对的环境变量

在配置信息的帮助下,我们将继续部署。

构建和部署

一旦我们完成配置,就可以进行部署。Zappa 提供了两个不同的命令来执行部署,例如zappa deploy <stage_name>zappa update <stage_name>。最初,我们将使用zappa deploy <stage_name>命令,因为这是我们第一次部署这个 Lambda 应用程序。

如果您已经部署了应用程序并希望重新部署它,那么您将使用zappa update <stage_name>命令。在上一章中,我们对 Zappa 的部署过程进行了详细讨论,因此如果需要,您可以参考这一点。

以下是我们部署过程的截图:

如您所见,成功部署后,我们得到了 API 网关端点 URL。让我们通过访问所述 URL 的管理面板来检查部署过程。看一下这个截图:

哎呀!我们遇到了一个错误。这个错误说我们有一个无效的HTTP_HOST,这是真的,因为我们没有在settings.py文件的ALLOWED_HOSTS列表中配置它,如下所述:

ALLOWED_HOSTS = ['localhost', 'cfsla2gds0.execute-api.ap-south-1.amazonaws.com']

这将解决问题。现在,让我们继续查看管理面板:

哎呀!看起来好像我们未能加载静态内容。但是我们已经使用 Amazon S3 和 Amazon CloudFront 配置了静态和媒体内容。

因此,为了解决这个错误,我们需要运行python manage.py collectstatic命令。这个命令将把所有静态内容上传到 Amazon S3,并通过 Amazon CloudFront 可用。看一下这个截图:

哇!我们解决了问题,我们的应用程序已经上线并且是无服务器的。部署真的很容易。希望您喜欢基于 Django 的应用程序的部署。

在这里,我们从未涉及任何服务器软件,如 Apache 或 Nginx 等复杂的配置。Zappa 使得将应用程序部署为无服务器变得非常容易。

现在我们将看看使用 Zappa 还可以做些什么。请参考我们的下一节,了解更多精彩内容!

使用 Zappa 进行 Django 管理命令

Zappa 提供了一个功能,可以直接从您的终端控制台在部署的 Lamdba 实例上执行 Django 的manage命令操作。通过zappa manage <stage_name> <manage-command>,您可以执行并检查您的 Django 应用程序的状态。

以下是执行此命令的截图:

尽管有一些限制。它只适用于 Django 的manage命令,因此它只适用于 Django 项目。

要传递任何参数,您可以使用字符串格式的manage命令,例如:

$ zappa manage dev "migrate --fake-initial"

但对于那些需要用户输入的命令,例如createsuperuser,它将没有用处。因此,在这种情况下,您可以以字符串格式编写 Python 脚本,并将其作为参数传递给zappa invoke <env> '<raw_script>' --raw。看一下这个截图:

就是这样。

希望你喜欢。这让开发人员的生活变得轻松。因为我们正在处理无服务器环境,所以可能需要这些功能。

摘要

我们学会了如何构建一个无服务器的 Django 应用程序。Zappa 使构建操作变得非常容易,并帮助您进行无服务器部署,非常方便。

在实现无服务器 Django 应用程序时,我们涵盖了所有必要的细节。我解释了为这个应用程序编写的代码;我还在我们的 GitHub 存储库中分享了整个代码库(github.com/PacktPublishing/Building-Serverless-Python-Web-Services-with-Zappa/tree/master/chapter_5/imageGalleryProject)。

希望你喜欢这一章。在下一章中,我们将实现相同的应用程序,但作为一个 RESTful API,并看看我们会遇到什么挑战。

问题

  1. 什么是 Amazon CloudFront?

  2. pipenv 用于什么?

第六章:使用 Zappa 构建 Django REST API

在本章中,我们将使用 Django Rest Framework 创建一个 RESTful API。它将基于一个简单的 RESTful API,具有CRUD创建检索更新删除)操作。我们可以考虑之前开发的ImageGallery应用程序与 REST API 扩展。在这里,我们将为PhotoAlbum创建一个 API,用户可以通过 REST API 界面创建新相册以及图片。

本章我们将涵盖以下主题:

  • 安装和配置 Django REST 框架

  • 设计 REST API

  • 使用 Zappa 构建、测试和部署 Django 应用程序

技术要求

在继续之前,有一些技术先决条件需要满足。这些先决条件是设置和配置开发环境所必需的。以下是所需软件的列表:

  • Ubuntu 16.04/Mac/Windows

  • Python 3.6

  • Pipenv 工具

  • Django

  • Django Rest Framework

  • Django Rest Framework JWT

  • Django 存储

  • Django Imagekit

  • Boto3

  • Zappa

我们将在虚拟环境中安装这些软件包。在下一节中,我们将看到有关安装过程的详细信息。

安装和配置 Django REST 框架

我们已经在第五章的设置虚拟环境部分详细介绍了虚拟环境设置过程。您可以按照这些说明配置 pipenv 工具并为本章创建一个新的虚拟环境。让我们转到下一节,使用 pipenv 工具安装所需的软件包。

安装所需的软件包

我们将使用 Django REST 框架开发 REST API,因此我们需要使用pipenv install <package_name>命令安装以下软件包:

  • django

  • djangorestframework

  • djangorestframework-jwt

  • django-storages

  • django-imagekit

  • boto3

  • zappa

您可以通过在空格分隔的其他软件包之后提及其他软件包来一次安装多个软件包,例如pipenv install <package_one> <package_two> ...

安装这些软件包后,我们可以继续实施,并且将有以下提到的Pipfile

文件—Pipfile

[[source]]

url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[dev-packages]

[packages]

django = "*"
djangorestframework = "*"
django-storages = "*"
django-imagekit = "*"
"boto3" = "*"
zappa = "*"

[requires]

python_version = "3.6"

Pipenv 在Pipfile.lock文件中维护版本及其 git 哈希。所以我们不需要担心。

我们已经完成了配置开发环境,现在是时候实施 REST API 了。请继续关注下一节,我们将使用 Django Rest Framework 设计 REST API。

设计 REST API

我们将为我们的 ImageGallery 应用程序设计 REST API。我们使用 Django 的管理界面开发了这个应用程序。现在我们将通过 RESTful API 界面扩展 ImageGallery 应用程序的现有实现。在实施解决方案之前,让我们简要介绍一下 Django REST 框架。

什么是 Django Rest Framework?

Django Rest Framework 是一个开源库,旨在以乐观的方式实现 REST API。它遵循 Django 设计模式,使用不同的术语。您可以在其文档网站(www.django-rest-framework.org/#quickstart)找到快速入门教程。

Django Rest Framework 是强大的,支持 ORM 和非 ORM 数据源。它内置支持可浏览的 API 客户端(restframework.herokuapp.com/)和许多其他功能。

建议在生产环境中不要使用 Web Browsable API 界面。您可以通过在settings.py中设置渲染类来禁用它。

settings.py file.

文件—settings.py

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
    )
}

集成 REST 框架

要集成 Django REST Framework,您可以简单地使用 pipenv 包装工具进行安装,就像在之前设置虚拟环境的部分中提到的那样。安装完成后,您可以继续在INSTALLED_APPS设置中添加rest_framework。看一下这段代码:

INSTALLED_APPS = (
    ...
    'rest_framework',
)

如果您想要在登录和注销视图以及 Web 浏览 API 一起使用,那么您可以在根urls.py文件中添加以下 URL 模式:

urlpatterns = [
    ...
    url(r'^api-auth/', include('rest_framework.urls'))
]

就是这样!现在我们已经成功集成了 Django REST Framework,我们可以继续创建 REST API。在创建 REST API 之前,我们需要实现身份验证和授权层,以便我们的每个 REST API 都能免受未经授权的访问。

让我们在下一节看看如何使我们的 REST API 安全。敬请关注。

实施身份验证和授权

身份验证和授权是设计 REST API 时必须考虑的重要部分。借助这些层,我们可以防止未经授权的访问我们的应用程序。有许多类型的实现模式可用,但我们将使用JWTJSON Web Token)。在en.wikipedia.org/wiki/JSON_Web_Token上了解更多信息。JWT 对于实现分布式微服务架构非常有用,并且不依赖于集中式服务器数据库来验证令牌的真实性。

有许多 Python 库可用于实现 JWT 令牌机制。在我们的情况下,我们希望使用django-rest-framework-jwt库(getblimp.github.io/django-rest-framework-jwt/),因为它提供了对 Django Rest Framework 的支持。

我假设您在之前描述的虚拟环境部分设置环境时已经安装了这个库。让我们看看下一节应该如何配置django-rest-framework-jwt库。

配置 django-rest-framework-jwt

安装完成后,您需要在settings.py中添加一些与权限和身份验证相关的预定义类,如下面的代码片段所示。

文件—settings.py

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
} 

现在我们需要根据用户凭据添加获取令牌的 URL。在根urls.py中,我们将添加以下语句:

from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
#...

urlpatterns = [
    '',
    # ...

    path(r'api-token-auth/', obtain_jwt_token),
]

api-token-auth API 将在成功验证后返回一个 JWT 令牌,例如:

$ curl -X POST -d "username=admin&password=password123" http://localhost:8000/api-token-auth/

{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFiZHVsd2FoaWQiLCJleHAiOjE1MjYwNDUwNjgsImVtYWlsIjoiYWJkdWx3YWhpZDI0QGdtYWlsLmNvbSJ9.Iw0ZTtdZpsQqrKIkf2VKoWw91txYp9DLkBYMS9OPoCU"}

这个令牌可以通过添加授权标头和令牌来授权所有其他受保护的 API,如下所示:

$ curl -H "Authorization: JWT <your_token>" http://localhost:8000/protected-url/

还有其他用例,您可能需要对已发行的令牌执行许多操作。为此,您需要阅读django-rest-framework-jwt的文档(getblimp.github.io/django-rest-framework-jwt/)。

现在让我们开始为我们的 ImageGallery 应用程序实现 API。

实施序列化器

Django Rest Framework 设计了一个类似于 Django 表单模块的序列化器模块,用于实现 JSON 表示层。序列化器负责对数据进行序列化和反序列化;您可以在这里看到有关数据序列化的详细解释(www.django-rest-framework.org/tutorial/1-serialization/#creating-a-serializer-class)。

序列化程序模块有许多有用的类,例如SerializerModelSerializerHyperlinkedModelSerializer等(www.django-rest-framework.org/api-guide/serializers/)。每个类都具有类似的操作,但具有扩展功能。Serializer类用于设计类似于 Django 表单表示的自定义数据表示,ModelSerializer用于表示与 Django 的ModelFrom类似的模型类数据。HyperlinkedModelSerializer通过超链接表示扩展了ModelSerializer的表示,并使用主键来关联相关数据。

我们需要创建一个使用ModelSerializer的序列化程序类。看一下这段代码。

文件—gallery/serializers.py

from rest_framework import serializers
from gallery.models import PhotoAlbum, Photo

class PhotoSerializer(serializers.ModelSerializer):

    class Meta:
        model = Photo
        fields = ('id', 'image', 'created_at', 'updated_at')

class PhotoAlbumSerializer(serializers.ModelSerializer):

    class Meta:
        model = PhotoAlbum
        fields = ('id', 'name', 'photos', 'created_at', 'updated_at')
        depth = 1

在这里,我们创建了PhotoSerializerPhotoAlbumSerializer类,使用ModelSerializer类。这些序列化程序与模型类相关联;因此,数据表示将基于模型结构。

让我们继续下一节,我们将创建视图。

实现 viewsets

Photo and PhotoAlbum models.

文件—gallery/views.py

from rest_framework import viewsets
from gallery.models import Photo, PhotoAlbum
from gallery.serializers import PhotoSerializer, PhotoAlbumSerializer

class PhotoViewset(viewsets.ModelViewSet):

    queryset = Photo.objects.all()
    serializer_class = PhotoSerializer

    def get_queryset(self, *args, **kwargs):
        if 'album_id' not in self.kwargs:
            raise APIException('required album_id')
        elif 'album_id' in self.kwargs and \
                not Photo.objects.filter(album__id=self.kwargs['album_id']).exists():
                                            raise NotFound('Album not found')
        return Photo.objects.filter(album__id=self.kwargs['album_id'])

    def perform_create(self, serializer):
        serializer.save(album_id=int(self.kwargs['album_id']))

class PhotoAlbumViewset(viewsets.ModelViewSet):

    queryset = PhotoAlbum.objects.all()
    serializer_class = PhotoAlbumSerializer

在这里,您可以看到我们已经创建了与PhotoPhotoAlbum模型相关的两个不同的 viewsets 类。PhotoAlbum模型与Photo模型有一对多的关系。因此,我们将编写一个嵌套 API,例如albums/(?P<album_id>[0-9]+)/photos。为了根据album_id返回相关的照片记录,我们重写了get_queryset方法,以便根据给定的album_id过滤queryset

类似地,我们重写了perform_create方法,以在创建新记录时设置关联的album_id。我们将在即将到来的部分中提供完整的演示。

让我们看一下 URL 配置,我们在那里配置了嵌套 API 模式。

配置 URL 路由

Django REST Framework 提供了一个router模块来配置标准的 URL 配置。它自动添加了与所述 viewsets 相关的所有必需的 URL 支持。在这里阅读更多关于routers的信息:www.django-rest-framework.org/api-guide/routers/。以下是与我们的路由配置相关的代码片段。

文件—gallery/urls.py

from django.urls import path, include
from rest_framework import routers
from gallery.views import PhotoAlbumViewset, PhotoViewset

router = routers.DefaultRouter()
router.register('albums', PhotoAlbumViewset)
router.register('albums/(?P<album_id>[0-9]+)/photos', PhotoViewset)

urlpatterns = [
    path(r'', include(router.urls)),
]

在这里,我们创建了一个默认路由器,并注册了带有 URL 前缀的 viewsets。路由器将自动确定 viewsets,并生成所需的 API URL。

urls.py file.

文件—imageGalleryProject/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path('admin/', admin.site.urls),
    path(r'', include('gallery.urls')),
    path(r'api-token-auth/', obtain_jwt_token),
]

一旦您包含了gallery.urls模式,它将在应用程序级别可用。我们已经完成了实现,现在是时候看演示了。让我们继续下一节,我们将探索 Zappa 配置,以及在 AWS Lambda 上的执行和部署过程。

使用 Zappa 构建、测试和部署 Django 应用程序

Django 提供了一个轻量级的部署 Web 服务器,运行在本地机器的 8000 端口上。您可以在进入生产环境之前对应用程序进行调试和测试。在这里阅读更多关于它的信息(docs.djangoproject.com/en/2.0/ref/django-admin/#runserver)。

让我们继续下一节,我们将探索应用程序演示和在 AWS Lambda 上的部署。

在本地环境中执行

python manage.py runserver command:
$ python manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).

May 14, 2018 - 10:04:25
Django version 2.0.5, using settings 'imageGalleryProject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

现在是时候看一下您的 API 的执行情况了。我们将使用 Postman,一个 API 客户端工具,来测试 REST API。您可以从www.getpostman.com/下载 Postman 应用程序。让我们在接下来的部分中看到所有 API 的执行情况。

API 身份验证

在访问资源 API 之前,我们需要对用户进行身份验证并获取 JWT 访问令牌。让我们使用api-token-authAPI 来获取访问令牌。我们将使用curl命令行工具来执行 API。以下是curl命令的执行:

$ curl -H "Content-Type: application/json" -X POST -d '{"username":"abdulwahid", "password":"abdul123#"}' http://localhost:8000/api-token-auth/
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFiZHVsd2FoaWQiLCJleHAiOjE1Mjk1NjYxOTgsImVtYWlsIjoiYWJkdWx3YWhpZDI0QGdtYWlsLmNvbSJ9.QypghhspJrNsp-v_XxlZeQFi_Wsujqh27EjlJtOaY_4"}

在这里,我们收到了 JWT 令牌作为用户身份验证的响应。现在我们将使用这个令牌作为授权标头来访问其他 API 资源。

在 API "/albums/"上的 GET 请求

此 API 将列出PhotoAlbum模型的所有记录。让我们尝试使用 cRUL 命令以GET请求方法访问/album/ API,如下所示:

$ curl -i http://localhost:8000/albums/ 
HTTP/1.1 401 Unauthorized
Date: Thu, 21 Jun 2018 07:33:07 GMT
Server: WSGIServer/0.2 CPython/3.6.5
Content-Type: application/json
WWW-Authenticate: JWT realm="api"
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 58
Vary: Cookie

{"detail":"Authentication credentials were not provided."}

在这里,我们从服务器收到了 401 未经授权的错误,消息是未提供身份验证凭据。这就是我们使用 JWT 令牌身份验证机制保护所有 API 的方式。

现在,如果我们只是使用从身份验证 API 获取的访问令牌添加授权标头,我们将从服务器获取记录。以下是成功的 API 访问授权标头的 cURL 执行:

$ curl -i -H "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFiZHVsd2FoaWQiLCJleHAiOjE1Mjk1NjY4NjUsImVtYWlsIjoiYWJkdWx3YWhpZDI0QGdtYWlsLmNvbSJ9.Dnbwuf3Mu2kcfk8KrbC-ql94lfHzK0z_5TgCPl5CeaM" http://localhost:8000/albums/
HTTP/1.1 200 OK
Date: Thu, 21 Jun 2018 07:40:14 GMT
Server: WSGIServer/0.2 CPython/3.6.5
Content-Type: application/json
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 598

[
    {
        "created_at": "2018-03-17T22:39:08.513389Z",
        "id": 1,
        "name": "Screenshot",
        "photos": [
            {
                "album": 1,
                "created_at": "2018-03-17T22:47:03.775033Z",
                "id": 5,
                "image": "https://chapter-5.s3-ap-south-1.amazonaws.com/media/Screenshot/AWS_Lambda_Home_Page.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIXNW3FK64BZR3DLA%2F20180621%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=20180621T073958Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=721acd5b023e13132f606a3f72bd672bad95a0dcb24572099c4cb49cdc34df71",
                "updated_at": "2018-03-17T22:47:18.298215Z"
            }
        ],
        "updated_at": "2018-03-17T22:47:17.328637Z"
    }
]

正如您所看到的,我们通过提供授权标头从"/albums/" API 获取了数据。在这里,我们可以使用| python -m json.tool以 JSON 可读格式打印返回响应。

在 API "/albums/<album_id>/photos/"上的 POST 请求

现在我们可以向现有记录添加更多照片。以下是 cRUL 命令执行的日志片段,我们正在将图像文件上传到现有相册:

$ curl -i -H "Content-Type: multipart/form-data" -H "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFiZHVsd2FoaWQiLCJleHAiOjE1Mjk1NzE5ODEsImVtYWlsIjoiYWJkdWx3YWhpZDI0QGdtYWlsLmNvbSJ9.3CHaV4uI-4xwbzAVdBA4ooHtaCdUrVn97uR_G8MBM0I" -X POST -F "image=@/home/abdulw/Pictures/serverless.png" http://localhost:8000/albums/1/photos/ HTTP/1.1 201 Created
Date: Thu, 21 Jun 2018 09:01:44 GMT
Server: WSGIServer/0.2 CPython/3.6.5
Content-Type: application/json
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 450

{
    "created_at": "2018-06-21T09:02:27.918719Z",
    "id": 7,
    "image": "https://chapter-5.s3-ap-south-1.amazonaws.com/media/Screenshot/serverless.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJA3LNVLKPTEOWH5A%2F20180621%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=20180621T090228Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=4e28ef5daa6e1887344514d9953f17df743e747c32b532cde12b840241fa13f0",
    "updated_at": "2018-06-21T09:02:27.918876Z"
}

现在,您可以看到图像已上传到 AWS S3 存储,并且我们已经配置了 AWS S3 和 CloudFront,因此我们获得了 CDN 链接。让我们再次查看所有记录的列表:

$ curl -H "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFiZHVsd2FoaWQiLCJleHAiOjE1Mjk1NzIzNTYsImVtYWlsIjoiYWJkdWx3YWhpZDI0QGdtYWlsLmNvbSJ9.m2w1THn5Nrpy0dCi8k0bPdeo67OHNYEKO-yTX5Wnuig" http://localhost:8000/albums/ | python -m json.tool

[
    {
        "created_at": "2018-03-17T22:39:08.513389Z",
        "id": 1,
        "name": "Screenshot",
        "photos": [
            {
                "album": 1,
                "created_at": "2018-03-17T22:47:03.775033Z",
                "id": 5,
                "image": "https://chapter-5.s3-ap-south-1.amazonaws.com/media/Screenshot/AWS_Lambda_Home_Page.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJA3LNVLKPTEOWH5A%2F20180621%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=20180621T090753Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=832abe952870228c2ae22aaece81c05dc1414a2e9a78394d441674634a6d2bbf",
                "updated_at": "2018-03-17T22:47:18.298215Z"
            },
            {
                "album": 1,
                "created_at": "2018-06-21T09:01:44.354167Z",
                "id": 6,
                "image": "https://chapter-5.s3-ap-south-1.amazonaws.com/media/Screenshot/serverless.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJA3LNVLKPTEOWH5A%2F20180621%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=20180621T090753Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=90a00ad79f141c919d8e65474325534461cf837f462cb52a840afb3863b72013",
                "updated_at": "2018-06-21T09:01:44.354397Z"
            },
            {
                "album": 1,
                "created_at": "2018-06-21T09:02:27.918719Z",
                "id": 7,
                "image": "https://chapter-5.s3-ap-south-1.amazonaws.com/media/Screenshot/serverless.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJA3LNVLKPTEOWH5A%2F20180621%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=20180621T090753Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=90a00ad79f141c919d8e65474325534461cf837f462cb52a840afb3863b72013",
                "updated_at": "2018-06-21T09:02:27.918876Z"
            }
        ],
        "updated_at": "2018-03-17T22:47:17.328637Z"
    }
]

现在我们的应用程序已根据我们的要求实施。我们可以继续使用 Zappa 在 AWS Lambda 上部署应用程序。现在让我们转向下一节来配置 Zappa。

配置 Zappa

zappa_settings.json file:
{
    "dev": {
        "aws_region": "ap-south-1",
        "django_settings": "imageGalleryProject.settings",
        "profile_name": "default",
        "project_name": "imagegallerypro",
        "runtime": "python3.6",
        "s3_bucket": "chapter-5",
        "remote_env": "s3://important-credentials-bucket/environments.json"
    }
}

在这里,我们根据要求定义了配置。由于密钥定义了每个配置,我们可以看到它的用法:

  • aws_region:Lambda 将上传的 AWS 区域。

  • django_settings:Django 设置文件的导入路径。

  • profile_name:在~/.aws/credentials文件中定义的 AWS CLI 配置文件。

  • project_name:上传 Lambda 函数的项目名称。

  • runtime:Python 运行时解释器。

  • s3_bucket:创建 Amazon S3 存储桶并上传部署包。

  • remote_env:设置 Amazon S3 位置上传的 JSON 文件中提到的所有键值对的环境变量。

借助这些配置信息,我们将继续部署。

构建和部署

一旦我们完成配置,就可以进行部署。Zappa 提供了两个不同的命令来执行部署,例如zappa deploy <stage_name>zappa update <stage_name>。最初,我们将使用zappa deploy <stage_name>命令,因为这是我们第一次部署此 Lambda 应用程序。

如果您已经部署了应用程序并希望重新部署,那么您将使用zappa update <stage_name>命令。在上一章中,我们详细讨论了 Zappa 的部署过程,因此您可以参考它。

以下是我们部署过程的日志片段:

$ zappa update dev
(python-dateutil 2.7.3 (/home/abdulw/.local/share/virtualenvs/imageGalleryProject-4c9zDR_T/lib/python3.6/site-packages), Requirement.parse('python-dateutil==2.6.1'), {'zappa'})
Calling update for stage dev..
Downloading and installing dependencies..
 - pillow==5.1.0: Downloading
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.95M/1.95M [00:00<00:00, 7.73MB/s]
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading imagegallerypro-dev-1529573380.zip (20.2MiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 21.2M/21.2M [00:06<00:00, 2.14MB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading imagegallerypro-dev-template-1529573545.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.65K/1.65K [00:00<00:00, 28.9KB/s]
Deploying API Gateway..
Scheduling..
Unscheduled imagegallerypro-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled imagegallerypro-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://cfsla2gds0.execute-api.ap-south-1.amazonaws.com/dev
https://cfsla2gds0.execute-api.ap-south-1.amazonaws.com/dev.

让我们转到下一节,我们将在部署的应用程序上执行一些操作。

在生产环境中执行

一旦您成功部署了应用程序,您将获得托管应用程序链接。这个链接就是通过将 AWS API 网关与 Zappa 的 AWS Lambda 配置生成的链接。

现在您可以在生产环境中使用应用程序。身份验证 API 的屏幕截图在下一节中。

身份验证 API

正如我们在本地环境中看到的身份验证执行一样,在生产环境中也是一样的。以下是部署在 AWS Lambda 上的身份验证 API 执行的日志片段:

$ curl -H "Content-Type: application/json" -X POST -d '{"username":"abdulwahid", "password":"abdul123#"}' https://cfsla2gds0.execute-api.ap-south-1.amazonaws.com/dev/api-token-auth/
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFiZHVsd2FoaWQiLCJleHAiOjE1Mjk1NzQyOTMsImVtYWlsIjoiYWJkdWx3YWhpZDI0QGdtYWlsLmNvbSJ9.pHuHaJpjlESwdQxXMiqGOuy2_lpVW1X26RiB9NN8rhI"}

正如您在这里所看到的,功能不会对任何事物产生影响,因为应用程序正在无服务器环境中运行。让我们看看另一个 API。

对“/albums/”API 的 GET 请求

通过身份验证 API 获得的访问令牌,您有资格访问所有受保护的 API。以下是/albums/API 的GET请求的屏幕截图:

$ curl -H "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFiZHVsd2FoaWQiLCJleHAiOjE1Mjk1NzQ4MzgsImVtYWlsIjoiYWJkdWx3YWhpZDI0QGdtYWlsLmNvbSJ9.55NucqsavdgxcmNNs6_hbJMCw42mWPyylaVvuiP5KwI" https://cfsla2gds0.execute-api.ap-south-1.amazonaws.com/dev/albums/ | python -m json.tool

[
    {
        "created_at": "2018-03-17T22:39:08.513389Z",
        "id": 1,
        "name": "Screenshot",
        "photos": [
            {
                "album": 1,
                "created_at": "2018-03-17T22:47:03.775033Z",
                "id": 5,
                "image": "https://chapter-5.s3-ap-south-1.amazonaws.com/media/Screenshot/AWS_Lambda_Home_Page.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJA3LNVLKPTEOWH5A%2F20180621%2Fap-south-1%2Fs3%2Faws4_request&X-Amz-Date=20180621T094957Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=0377bc8750b115b6bff2cd5acc024c6375f5fedc6de35275ea1392375041adc0",
                "updated_at": "2018-03-17T22:47:18.298215Z"
            }
        ],
        "updated_at": "2018-03-17T22:47:17.328637Z"
    }
]

就是这样。我们已经完成了无服务器环境的部署。希望对您有所帮助。

总结

在本章中,我们学习了如何在 Django REST 框架中开发 REST API。我们介绍了使用 JWT 身份验证机制保护 API 的过程。最后,我们使用 Zappa 在无服务器环境中部署了应用程序。

在下一章中,我们将使用非常轻量级的 Python 框架开发基于高性能 API 的应用程序。我们还将探索更多 Zappa 配置选项,以建立缓存机制。敬请关注,发现 Zappa 世界中更多的宝藏。

问题

  1. 什么是 Django Rest 框架?

  2. Django-storage 有什么用?

第七章:使用 Zappa 构建猎鹰应用程序

在本章中,我们将实施一个基于猎鹰框架的应用程序。这个应用程序将与引用相关;您将能够获取每日引用和生成一个随机引用。我希望这对您来说是有趣的。我们将包括一个调度程序,它将负责从第三方 API 获取一个随机引用并将其放入我们的数据库中。我们将设置这个调度程序每天执行一次。让我们为这次旅行做好准备。

本章我们将涵盖以下主题:

  • 安装和配置猎鹰

  • 设计猎鹰 API

  • 使用 Zappa 构建、测试和部署猎鹰 API

技术要求

在本章的开发工作中继续之前,我想建议先满足设置开发环境的先决条件。以下是技术要求的列表:

  • Ubuntu 16.04/macOS/Windows

  • Python 3.6

  • Pipenv 工具

  • 猎鹰

  • Peewee

  • 请求

  • Gunicorn

  • Zappa

在下一节中,我已经描述了设置环境的完整信息。让我们为此做好准备,探索通往无服务器的旅程。

安装和配置猎鹰

配置 Python 应用程序开发需要我们设置虚拟环境。借助虚拟环境,我们将维护所有所需的软件包。正如在第六章中讨论的那样,使用 Zappa 构建 Django REST API,pipenv 打包工具在虚拟环境中维护所有已安装的软件包,并跟踪版本和依赖项。让我们继续使用 pipenv 工具设置虚拟环境。

设置虚拟环境

在开始实际实施之前,我们将使用 pipenv 工具设置虚拟环境。以下是创建新虚拟环境的命令:

$ pipenv --python python3.6

在这里,我明确提到了 Python 版本,因为我在我的系统上使用了许多其他 Python 版本。这个命令将创建一个Pipfile,如下所示:

[[source]]

url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[dev-packages]

[packages]

[requires]

python_version = "3.6"

正如您所见,前面的代码中包含了关于环境的基本信息,但在软件包下面没有任何内容,因为我们还没有安装任何软件包。这个文件维护了所有已安装软件包的列表。pipenv 工具在~/.local/share/virtualenvs/创建一个虚拟环境,并且当我们调用前面的命令时,它将从该目录创建新的环境。一旦您执行了该命令,就会创建Pipfile,如前所述。

您可以执行pipenv shell命令来启用虚拟环境。让我们继续下一节,我们将安装所有所需的软件包。

安装所需的软件包

正如我们之前提到的,我们将创建一个基于猎鹰的 API 应用程序。因此,我们需要安装一些我们将在实现中使用的软件包。以下是我们将在实现中使用的软件包列表:

  • falcon

  • zappa

  • gunicorn

  • peewee

  • requests

您可以使用pipenv install <package_name>命令安装这些软件包。

您可以通过指定其他以空格分隔的软件包一次安装多个软件包,例如pipenv install <package_one> <package_two> ...

一旦您安装了所有这些软件包,pipenv 将创建一个名为Pipfile.lock的文件,其中包含版本和依赖项的信息。Pipfile将被更新。

Pipfile:

文件—Pipfile

[[source]]

url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[dev-packages]

pylint = "*"

[packages]

falcon = "*"
zappa = "*"
gunicorn = "*"
peewee = "*"
requests = "*"

[requires]

python_version = "3.6"

现在我们已经完成了虚拟环境的设置,是时候开始实施应用程序了。但在继续设置环境之前,让我们先了解一些重要的软件包及其用法。

什么是猎鹰?

猎鹰是一个裸金属 Python Web API 框架。它可以用于构建具有非常快速性能的微服务。

它非常灵活和易于实现。与其他框架相比,它具有显著的基准。有许多大型组织正在使用 Falcon,如领英、OpenStack、RackSpace 等。以下是 Falcon 网站上的示例代码片段:

# sample.py

import falcon

class QuoteResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        quote = {
            'quote': (
                "I've always been more interested in "
                "the future than in the past."
            ),
            'author': 'Grace Hopper'
        }

        resp.media = quote

api = falcon.API()
api.add_route('/quote', QuoteResource())

它需要gunicorn在本地主机上执行 API,如下面的代码块所示:

$ gunicorn sample:api

Falcon 真的很简单,而且在 Falcon 中更容易实现 REST API,因为它鼓励我们遵循 REST 架构风格。您可以在此处阅读有关 Falcon 的更多信息:falconframework.org/#

什么是 Peewee?

Peewee 是一个简单而小巧的ORM对象关系映射器)。它旨在提供类似于 Django 或 SQLAlchemy 的 ORM 接口。它支持 MySQL、Postgres 和 SQLite 等数据库。

以下是 Peewee 的 GitHub 页面上的定义模型类的示例代码片段:

from peewee import *
import datetime

db = SqliteDatabase('my_database.db')

class BaseModel(Model):
    class Meta:
        database = db

class User(BaseModel):
    username = CharField(unique=True)

class Tweet(BaseModel):
    user = ForeignKeyField(User, backref='tweets')
    message = TextField()
    created_date = DateTimeField(default=datetime.datetime.now)
    is_published = BooleanField(default=True)

这真的很棒——我们以 Django 风格设计数据库模型的可行性与一个小包装器。Peewee 真的很棒,可以考虑用于编写小型微服务。

在此处阅读有关 Peewee 的更多信息:docs.peewee-orm.com/en/latest/

让我们继续下一节,我们将在实际中使用 Falcon 和 Peewee。

设计 Falcon API

我们将基于报价概念设计一个 REST API。报价可能是名人说的话,也可能是电影中的对话。我们将使用 Mashape 的随机名人名言API(market.mashape.com/andruxnet/random-famous-quotes)。Mashape 是一个 API 平台,提供许多类别的 API。

在我们的情况下,我们将创建一个包含以下操作的单个 API:

  • 生成或检索当天的报价

  • 生成随机报价

对于第一个操作,我们将需要每天将来自 Mashape API 的随机报价存储到我们的数据库中。因此,我们需要设计一个任务调度程序,以便每天执行并将来自 Mashape API 的报价存储到我们的数据库中,以便我们的 API 用户可以获得当天的报价。

对于第二个操作,我们不需要将从 Mashape API 随机生成的每一条报价都持久化。相反,我们将生成的随机报价返回给我们的 API 用户。

搭建应用程序

在设计任何应用程序时,搭建是在实施解决方案之前必须考虑的重要步骤。它帮助我们以一种乐观的方式管理代码库。以下是我们应用程序的搭建:

在这里,我们根据功能将代码库分成不同的模块。让我们在接下来的部分中看一下每个模块。

设计模型类

models.py.

文件—models.py

import os
import datetime
from shutil import copyfile
from peewee import *

# Copy our working DB to /tmp..
db_name = 'quote_database.db'
src = os.path.abspath(db_name)
dst = "/tmp/{}".format(db_name)
copyfile(src, dst)

db = SqliteDatabase(dst)

class QuoteModel(Model):

    class Meta:
        database = db

    id = IntegerField(primary_key= True)
    quote = TextField()
    author = CharField()
    category = CharField()
    created_at = DateTimeField(default= datetime.date.today())

db.connect()
db.create_tables([QuoteModel])

在这里,我们通过扩展Model类定义了QuoteModel,并使用 Peewee 库的特性定义了属性。这里最重要的部分是数据库连接;正如您所看到的,我们使用了 SQLite 数据库。我们创建了数据库文件并将其放在/tmp目录中,以便在 AWS Lambda 环境中可以访问。

一旦我们使用SqliteDatabase类定义了数据库,我们就连接数据库并根据模型定义创建数据库表。

db.create_tabless方法只在表不存在时创建表。

现在我们准备使用这个Model类来执行任何查询操作。但是,在创建资源之前,让我们看一下mashape.py,在那里我们集成了第三方 API 以获取随机报价。

Mashape API 集成

Mashape 是私人和公共 API 的最大 API 市场。有数千个 API 提供者和消费者注册。请查看市场market.mashape.com。我们将使用随机名言引用 API(market.mashape.com/andruxnet/random-famous-quotes)。一旦您登录 Mashape 市场,您可以详细了解这些 API。以下代码片段是我们用来获取随机引用的 API 之一。

mashape.py file.

文件 - mashape.py

import os
import requests

def fetch_quote():
    response = requests.get(
        os.environ.get('Mashape_API_Endpoint'),
        headers={
            'X-Mashape-Key': os.environ.get('X_Mashape_Key'),
            'Accept': 'application/json'
        }
    )
    if response.status_code == 200:
        return response.json()[0]
    return response.json()

在这里,我们编写了一个名为fetch_quote的方法。此方法负责从 Mashape API 获取引用并以 Python 字典格式返回引用数据。根据我们的需求,我们将在不同的地方使用此方法。

创建 API 资源

resources.py.

文件 - resources.py

import os
import datetime
import requests
import falcon

from models import QuoteModel
from mashape import fetch_quote

class QuoteResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        if req.get_param('type') in ['daily', None]:
            data = QuoteModel.select().where(QuoteModel.created_at == datetime.date.today())
            if data.exists():
                data = data.get()
                resp.media = {'quote': data.quote, 'author': data.author, 'category': data.category}
            else:
                quote = fetch_quote()
                QuoteModel.create(**quote)
                resp.media = quote
        elif req.get_param('type') == 'random':
            resp.media = fetch_quote()
        else:
            raise falcon.HTTPError(falcon.HTTP_400,'Invalid Quote type','Supported types are \'daily\' or \'random\'.')

api = falcon.API()
api.add_route('/quote', QuoteResource())

在这里,我们创建了QuoteResource类,并实现了on_get方法来处理GET请求。为了执行生成每日引用和随机引用的不同操作,我们定义了一个名为type的查询参数,例如,http://<API_URL>?type=daily|random。因此,根据查询参数,我们提供服务。

我们已经完成了实施。我们将在下一节中查看执行、调度和部署。

使用 Zappa 构建、测试和部署 Falcon API

与其他框架无关,Falcon 需要gunicorn库进行执行。Gunicorn 是一个轻量级的 Python WSGI HTTP 服务器。Falcon 没有任何默认行为来提供 WSGI 服务;相反,Falcon 主要关注 API 架构风格和性能。让我们继续在本地环境中执行 API。

使用 gunicorn 进行本地执行

对于本地执行,我们将使用gunicorn。以下是gunicorn执行的日志:

$ gunicorn resources:api
[2018-05-18 15:40:57 +0530] [31655] [INFO] Starting gunicorn 19.8.1
[2018-05-18 15:40:57 +0530] [31655] [INFO] Listening at: http://127.0.0.1:8000 (31655)
[2018-05-18 15:40:57 +0530] [31655] [INFO] Using worker: sync
[2018-05-18 15:40:57 +0530] [31662] [INFO] Booting worker with pid: 31662

我们正在使用resources模块和api对象进行执行。我们使用resources模块创建了api对象。

每日引用的 API

我们实现了/quote API,并根据查询参数分离了操作。让我们执行/quote?type=daily API。以下是使用 cURL 命令行工具执行每日引用 API 的日志片段:

$ curl http://localhost:8000/quote?type=daily
{"quote": "I'll get you, my pretty, and your little dog, too!", "author": "The Wizard of Oz", "category": "Movies"}

此 API 将每天返回一个独特的引用。

随机引用的 API

现在,让我们对/quote API 执行另一个操作,例如/quote?type=random。此 API 将在每个请求上返回一个随机引用。以下是 API 执行的日志:

$ curl http://localhost:8000/quote?type=random
{"quote": "The only way to get rid of a temptation is to yield to it.", "author": "Oscar Wilde", "category": "Famous"}

此 API 将在每个请求上返回一个随机引用记录。

配置 Zappa

一旦我们在设置虚拟环境时安装了 Zappa,我们就可以配置 Zappa 与我们的应用程序。以下是我们将执行的操作,以配置 Zappa。

Zappa 初始化

settings.json file.

文件 - zappa_settings.json

{
    "dev": {
        "app_function": "resources.api",
        "aws_region": "ap-south-1",
        "profile_name": "default",
        "project_name": "chapter-7",
        "runtime": "python3.6",
        "s3_bucket": "zappa-0edixmwpd",
        "remote_env": "s3://book-configs/chapter-7-config.json"
    }
}

在这里,我们根据要求定义了配置。由于密钥定义了每个配置,我们可以看到它的用法:

  • aws_region: lambda 将上传的 AWS 区域

  • app_function: 从resources模块导入api对象的导入路径

  • profile_name: 在~/.aws/credentials文件中定义的 AWS CLI 配置文件

  • project_name: 上传 lambda 函数的项目名称。

  • runtime: Python 运行时解释器

  • s3_bucket: 创建一个 Amazon S3 存储桶并上传部署包。

  • remote_env: 在 Amazon S3 位置上传的 JSON 文件中设置所有键值对的环境变量

借助这些配置信息,我们可以继续部署。

Zappa 部署

一旦配置完成,我们就可以进行部署。Zappa 提供了两个不同的命令来执行部署,zappa deploy <stage_name>zappa update <stage_name>。最初,我们使用zappa deploy <stage_name>命令,因为这是我们首次部署此 lambda 应用程序。

如果您已经部署了应用程序并希望重新部署,则可以使用zappa update <stage_name>命令。在前一章中,我们对 Zappa 的部署过程进行了详细讨论,因此您可以参考那里获取更多信息。

以下是我们部署过程的日志:

$ zappa update dev
Important! A new version of Zappa is available!
Upgrade with: pip install zappa --upgrade
Visit the project page on GitHub to see the latest changes: https://github.com/Miserlou/Zappa
Calling update for stage dev..
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter-7-dev-1529584381.zip (5.9MiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 6.17M/6.17M [00:03<00:00, 1.08MB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading chapter-7-dev-template-1529584474.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 9.09KB/s]
Deploying API Gateway..
Scheduling..
Unscheduled chapter-7-dev-schedulers.set_quote_of_the_day.
Unscheduled chapter-7-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled chapter-7-dev-schedulers.set_quote_of_the_day with expression cron(0 12 * * ? *)!
Scheduled chapter-7-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://0uqnn5ql3a.execute-api.ap-south-1.amazonaws.com/dev

在这里,我使用zappa update dev来部署我的现有应用程序。此命令将在最后打印部署的 URL;我们可以使用它在生产环境中测试应用程序。

在生产环境中执行

由于我们使用 Zappa 在 AWS Lambda 上部署了应用程序,Zappa 配置了具有代理到 AWS Lambda 的 API Gateway。因此,它将具有在前一节中提到的随机生成的 API Gateway 链接。

现在,让我们使用生成的链接执行我们的 API(0uqnn5ql3a.execute-api.ap-south-1.amazonaws.com/dev/quote)。

每日引用 API 执行

执行操作将类似于本地执行,但它将对 API Gateway 产生一些影响,因为 AWS API Gateway 中有许多可用于增强 API 性能和优化的功能。

以下是使用 cURL 工具执行每日引用 API 的日志片段:

$ curl https://0uqnn5ql3a.execute-api.ap-south-1.amazonaws.com/dev/quote?type=daily
{"quote": "You've got to ask yourself one question: 'Do I feel lucky?' Well, do ya, punk?", "author": "Dirty Harry", "category": "Movies"}

我们的应用程序作为无服务器应用程序正在运行。您可以使用它而不必过多担心服务器,因为它能够每秒提供数百万次请求,并且亚马逊将负责其可伸缩性和可用性。让我们尝试另一个 API。

随机引用 API 执行

让我们执行随机引用 API。以下是随机引用 API 执行的片段:

$ curl -s -w 'Total time taken: %{time_total}\n' https://0uqnn5ql3a.execute-api.ap-south-1.amazonaws.com/dev/quote?type=random
{"quote": "A friendship founded on business is better than a business founded on friendship.", "author": "John D. Rockefeller", "category": "Famous"}
Total time taken: 1.369

您可以看到此执行需要 1.369 秒,因为我们明确发出了另一个请求到 Mashape API 以获取随机引用。通过为 API Gateway 服务添加缓存支持,我们可以使此执行更快。

在 API Gateway 上启用缓存

AWS API Gateway 提供了一个功能,可以为 API 端点响应添加缓存。它将有助于减少网络延迟,并向用户返回缓存的响应,而无需触发 AWS Lambda 函数。

Zappa 具有配置 AWS API Gateway 上缓存的能力;您无需手动从 AWS Web 控制台配置缓存。以下是在zappa_settings.json文件中添加的配置,以启用 API Gateway 上的缓存。

文件—zappa_settings.json

{
    "dev": {
        "app_function": "resources.api",
        "aws_region": "ap-south-1",
        "profile_name": "default",
        "project_name": "chapter-7",
        "runtime": "python3.6",
        "s3_bucket": "zappa-0edixmwpd",
        "remote_env": "s3://book-configs/chapter-7-config.json",
        "cache_cluster_enabled": false,
 "cache_cluster_size": 0.5,
 "cache_cluster_ttl": 300,
 "cache_cluster_encrypted": false,
    }
}

如前所述,在zappa_settings.json文件中的缓存选项。让我们看看它的用法:

  • cache_cluster_enabled:默认为false;此选项设置为true以启用 API Gateway 缓存集群。

  • cache_cluster_size:默认为 0.5 GB;这表示缓存内存大小。如果需要,我们也可以增加大小。

  • cache_cluster_ttl:默认为 300 秒;此选项用于设置内存中响应缓存的生存时间(TTL)。最大限制为 3,600 秒,如果要禁用它,可以将其设置为 0 秒。

  • cache_cluster_encrypted:默认为false;如果要加密缓存的响应数据,则将此选项设置为true

这就是您可以在没有任何手动干预的情况下启用 API Gateway 缓存机制的方法。只有GET请求方法应该被缓存。

AWS API Gateway 不支持免费套餐。它按小时计费。在aws.amazon.com/api-gateway/pricing/上阅读有关 API Gateway 定价的更多信息。

事件调度

AWS Lambda 可以与 AWS CloudWatch 事件一起配置。如果要定期执行 Lambda 函数,例如,每五分钟执行一次,您可以使用速率表达式,或者可以配置cron表达式以安排定时事件进行执行。

您可以在docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html阅读有关计划表达式的更多信息。

配置 AWS Lambda 与计划事件需要更多的手动干预。您可以查看官方文档docs.aws.amazon.com/lambda/latest/dg/with-scheduled-events.html

Zappa 提供了一种非常灵活的方式来配置计划事件,无需任何手动干预。

使用 Zappa 配置事件

Zappa 支持定时事件和 AWS 事件。定时事件与时间和日期相关,而 AWS 事件与任何 AWS 服务相关,例如 AWS S3 事件等。

我们可以根据任何 AWS 事件安排 Lambda 函数的执行,如下面的代码片段所示:

{
    "production": {
       ...
       "events": [{
            "function": "your_module.process_upload_function",
            "event_source": {
                  "arn": "arn:aws:s3:::my-bucket",
                  "events": [
                    "s3:ObjectCreated:*" 
                  ]
               }
            }],
       ...
    }
}

Zappa 支持几乎所有 AWS 事件来执行 AWS lambda 函数。您可以在github.com/Miserlou/Zappa#executing-in-response-to-aws-events阅读有关响应 AWS 事件执行的更多信息。

一旦添加了事件配置,您可以执行以下命令来安排事件:

$ zappa schedule production 

在我们的案例中,我们将安排一个有时间限制的事件来执行一个函数,以获取每日报价并将其存储在数据库中。让我们看看如何配置我们的应用程序以安排每日事件。

安排一个事件来设置每日报价

由于我们已经设计了/quote?type=daily API 来获取每日报价,如果该报价存在于数据库中,则此 API 将返回该报价,否则将从 Mashape API 获取并将其存储在数据库中。此操作是为了防止 API 在数据库中不存在报价记录的情况下失败。

但是我们想要确保报价记录确实存在于数据库中。为此,我们将安排一个每日事件,将在午夜发生。我们将执行一个函数来执行获取报价操作。

以下是带有事件配置的 Zappa 设置片段。

文件—zappa_settings.json

{
    "dev": {
        "app_function": "resources.api",
        "aws_region": "ap-south-1",
        "profile_name": "default",
        "project_name": "chapter-7",
        "runtime": "python3.6",
        "s3_bucket": "zappa-0edixmwpd",
        "remote_env": "s3://book-configs/chapter-7-config.json",
        "cache_cluster_enabled": false,
        "cache_cluster_size": 0.5,
        "cache_cluster_ttl": 300,
        "cache_cluster_encrypted": false,
        "events": [{
 "function": "schedulers.set_quote_of_the_day",
 "expression": "cron(0 12 * * ? *)"
 }]
    }
}
schedulers module.

文件—schedulers.py

from models import QuoteModel
from mashape import fetch_quote

def set_quote_of_the_day(event, context):
    QuoteModel.create(**fetch_quote())
set_quote_of_the_day will be executed by the scheduled event and will perform the operation to fetch the quote and store it in the database.

现在,为了启用计划事件,让我们运行zappa schedule dev命令。以下是schedule命令执行的日志:

$ zappa schedule dev
Calling schedule for stage dev..
Scheduling..
Unscheduled chapter-7-dev-zappa-keep-warm-handler.keep_warm_callback.
Unscheduled chapter-7-dev-schedulers.set_quote_of_the_day.
Scheduled chapter-7-dev-schedulers.set_quote_of_the_day with expression cron(0 12 * * ? *)!
Scheduled chapter-7-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!

就是这样;我们现在已经完成了调度。现在,每天午夜,set_quote_of_the_day方法将被调用并执行获取报价的操作。

总结

在本章中,我们学习了如何基于 Falcon 框架创建高性能的 API。我们还学习了如何使用 Zappa 配置 API Gateway 缓存机制。我们涵盖的最有趣的部分是调度。现在,您不需要担心任何第三方调度工具,因为 Zappa 使基于时间和 AWS 事件的调度变得非常容易。

希望您喜欢本章。现在让我们进入下一章,探索 Zappa 的功能。我们将为我们的应用程序设置自定义域和 SSL 证书。

问题

  1. Falcon 与其他 Python 框架有什么不同?

  2. Peewee 库相对于SQLAlchemy的好处是什么?

  3. 调度是如何工作的?

第八章:带 SSL 的自定义域

在本章中,我们将为上一章开发的报价应用程序配置自定义域。配置自定义域是将应用程序移动到生产环境的重要部分,因为它是无服务器的。这个过程涉及多个操作,与传统的 Apache 或 NGINX 配置不同。我们将查看已部署在无服务器基础架构中的报价应用程序。

本章我们将涵盖的主题包括:

  • 使用 AWS Route53 配置自定义域

  • 使用 Amazon 证书管理器生成 SSL 证书

  • 使用 Zappa 集成自定义域

技术要求

在开始本章之前,有一些先决条件需要满足。我们将使用一些 AWS 服务和一个真实的域名。因此,您将需要以下内容:

  • Ubuntu 16.04/Windows/macOS

  • Pipenv 工具

  • Zappa 和其他 Python 开发包

  • 注册域名

  • AWS 账户

我们将使用一些 Python 包,这些包在后面的部分中提到。除了开发环境,您还需要拥有自己注册的域名和更新其默认域名服务器的权限。让我们转到下一节,在那里我们将探索与 AWS Route 53 的域名服务器配置。

使用 AWS Route 53 配置自定义域

为我们的应用程序创建自定义域需要拥有一个域。域名可以从域名注册商那里购买。在我们的情况下,我从GoDaddy(in.godaddy.com/),这个域名系统(DNS)服务提供商那里购买了一个名为abdulwahid.info的域名。

每个域通过 DNS 服务提供商管理的域名服务器在互联网上提供服务。有许多服务提供商提供服务,可以从他们的端口管理和托管网站。我们将使用 AWS Route 53 服务。

什么是 AWS Route 53?

AWS Route 53 是一种可扩展的云 DNS 网络服务。Route 53 在配置与任何 AWS 服务的域名方面非常有效。它连接到在 AWS 上运行的基础架构以及 AWS 之外的基础架构。Route 53 提供各种路由,如基于延迟的路由、地理 DNS、地理近似和加权轮询。所有这些路由可以组合在一起,以提供低延迟带宽。Route 53 还提供域名注册服务。如果我们在 AWS Route 53 上注册域名,那么我们就不需要管理 DNS 配置。所有 DNS 配置将自动使用 AWS 服务。

但我们没有在 Route 53 上注册我们的域,所以我们需要用 Route 53 替换默认的 GoDaddy 域名服务器。在下一节中,我们将讨论如何更改域名服务器。

将域名服务器更改为 Route 53

我们将把现有域的控制权转移到 Route 53。这个过程需要将域名abdulwhaid.info的默认域名服务器更改为 Route 53 创建的新域名服务器。

参考 AWS 官方文档(docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingHostedZone.html)关于在不同的注册商上为 Route 53 配置现有域名创建托管区域,执行以下步骤:

  1. 登录 AWS 控制台,在console.aws.amazon.com/route53/.打开 Route 53 控制台

  2. 如果您是 Route 53 的新用户,请在 DNS 管理下选择立即开始

  3. 如果您已经使用 Route 53,请在左侧导航窗格中选择托管区域,如下面的屏幕截图所示:

  1. 现在,从托管区域页面,点击使用域abdulwahid.info创建托管区域,如下面的屏幕截图所示:

  1. 一旦您为域名 abdulwahid.info 创建了托管区域,Route 53 将创建两个记录,域名服务器 (NS) 和 授权起始 (SOA),如下截图所示:

  1. 现在,我们需要使用 NS 记录并替换在域名注册商(即 GoDaddy)生成的默认 NS 记录,在那里我们创建了域名 abdulwahid.info。以下是默认 NS 记录的截图:

  1. 将默认 NS 更改为自定义,并输入在 Route 53 生成的 NS 记录,如下截图所示:

  1. 单击保存,我们完成了。现在需要一些时间由域名注册商处理。您将收到来自域名注册商的确认电子邮件。

Route 53 通过特定域名的托管区域管理路由流量。托管区域就像一个容器,包含有关域名的信息,并知道如何在互联网上路由流量。

一旦您收到确认电子邮件,域名 abdulwahid.info 将由 Route 53 管理。让我们转到下一节,了解如何使用 AWS 证书管理器配置 SSL 证书。

使用 AWS 证书管理器生成 SSL 证书

SSL 为您的 Web 服务器和应用程序用户提供安全性。借助 SSL,您可以防止黑客对在 Web 服务器和浏览器之间通信的数据进行攻击。在将 SSL 安全性应用到我们的应用程序之前,让我们了解一些关于 SSL 的基本方法。

SSL 是什么?

SSL (安全套接字层) 是一种标准的安全协议,用于通过加密数据保护 Web 服务器和浏览器之间的通信。SSL 将确保从浏览器传输到您的 Web 服务器的数据是加密的。为了创建 SSL 连接,我们需要生成 SSL 证书并配置我们的 Web 服务器以在 SSL 层下提供服务。下一节将讨论 SSL 证书。

什么是 SSL 证书?

为了创建 SSL 连接,我们需要一个 SSL 证书。SSL 证书可以从 证书颁发机构 (CA) 生成。在生成证书之前,我们需要提供有关我们的网站和业务详细信息。根据这些信息,将生成两个加密密钥:公钥和私钥。

现在,使用公钥和业务详细信息,我们需要与 CA 处理一个 证书签名请求 (CSR)。一旦 CA 成功授权我们的详细信息,它将颁发与我们的私钥匹配的 SSL 证书。

现在,我们准备为我们的应用程序配置 SSL 证书。这是生成 SSL 证书的传统方式。但是我们将使用 Amazon 证书管理器来生成 SSL 证书。

使用 Amazon 证书管理器 (ACM) 生成 SSL 证书

有几种生成 SSL 证书的方法。以下是一些获取应用程序的 SSL/TSL 证书的方法:

  • 您可以从 SSL 证书颁发机构购买 SSL 证书。

  • 您可以通过使用 Let's Encrypt (letsencrypt.org/) 自行生成免费的 SSL/TSL 证书。Let's Encrypt 是一个提供免费 SSL/TSL 证书的开放式证书颁发机构。

  • 您可以使用 AWS 证书管理器 (ACM) 生成 SSL。我们将使用 ACM 为我们的应用程序生成 SSL 证书。

ACM 是一个管理和创建 AWS 服务和应用程序的 SSL/TSL 证书的服务。ACM 证书适用于多个域名和子域名。您还可以使用 ACM 创建通配符 SSL。

ACM 严格与 AWS 证书管理器私有证书颁发机构 (ACM PCA) 相关联。ACM PCA 负责验证域名授权并颁发证书。

现在,我们将为我们的域和子域生成一个 ACM 证书。按照以下步骤创建 ACM 证书:

请注意,API Gateway 仅支持来自一个地区的 ACM 证书。因此,我们将使用US East地区。您可以在github.com/Miserlou/Zappa/pull/1142上阅读更多信息。

  1. 登录 AWS 控制台,在ap-south-1.console.aws.amazon.com/acm打开 ACM 控制台。

  2. 如果您是 AWS ACM 的新用户,请在“Provision certificates”下点击“Get Started”,如果您已经在使用 AWS ACM,请选择“Request a certificate”,如下面的截图所示:

在这里,我们将选择请求公共证书。

您可以在docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html上阅读更多关于公共证书的信息。

  1. 在下一页,您需要提供您的域名的详细信息。我们将使用通配符(*)作为子域名来针对我们的域请求一个通配符证书。因此,这个证书可以用来保护同一域名下的多个站点。以下是添加域名的截图:

  1. 在下一页,您需要选择验证方法。有两种类型的方法可用,如下所示:
  • DNS 验证:此方法需要修改证书中域的 DNS 记录的权限,以便它可以直接验证记录集。

  • 电子邮件验证:如果您没有权限修改 DNS 记录,则可以使用此方法。因此,您可以使用与域名注册商记录的注册电子邮件来验证域。

我们将使用 DNS 验证方法。这是因为我们拥有 Route 53 托管区中的 DNS 访问权限,这是因为在域名注册商处有映射的域名服务器。DNS 验证很简单。请看下面的截图:

  1. 现在,我们已经准备好了。点击“Review”将显示所选的配置,如下面的截图所示:

  1. 一旦您从“Review”页面点击“确认并请求”,您需要完成验证过程。下面的截图显示验证状态为待定,因此我们需要通过展开域部分来执行验证:

  1. 展开域部分后,您将看到一些完成验证过程的说明。我们选择了 DNS 验证方法。因此,这种方法需要向 DNS 配置添加一个 CNAME 记录。根据下面的截图,您可以通过点击“在 Route 53 中创建记录”按钮来执行更新 DNS 配置的操作,以给定的 CNAME:

  1. 一旦您点击了在 Route 53 中创建记录,它将弹出一个确认弹窗,显示 CNAME 记录,如下面的截图所示:

  1. 点击“创建”按钮后,它会自动使用给定的 CNAME 记录更新 Route 53 中的 DNS 配置。您将看到成功消息,如下面的截图所示:

  1. 点击“继续”,我们完成了。您将被重定向到证书仪表板页面,如下面的截图所示:

ACM CA 已成功为您的域颁发了 SSL 证书。正如您所看到的,绿色状态中显示了“已颁发”。现在,是时候配置域和证书与我们的应用程序了。在下一节中,我们将使用我们的报价 API 应用程序配置一个子域与已颁发的 SSL 证书。

使用 Zappa 集成自定义域

Zappa 支持自定义域名和子域集成与 SSL 证书。我们已经在前几节中讨论了 SSL/TSL 证书生成的来源。Zappa 可以使用以下 CA 部署域:

  • 您自己从证书颁发机构提供商购买的 SSL

  • Let's Encrypt

  • AWS

您可以在以下链接中阅读有关使用上述 CA 部署域的更多详细信息:github.com/Miserlou/Zappa#ssl-certification

我们将使用 AWS 证书颁发机构 SSL 证书。我们已经在上一节中生成了 ACM 证书。现在是时候将 ACM 证书与我们的应用程序的子域集成了。

让我们转到下一节,在那里我们将使用子域和 ACM 证书配置我们的报价 API 应用程序。

使用 ACM 证书部署到域

由于我们已经颁发了 ACM 证书,现在让我们将应用程序配置到所需的域并执行部署过程。Zappa 提供了一个domain属性来配置应用程序的域名和certificate_arn用于 ACM 证书。您需要在zappa_settings.json中配置这两个属性。

在此之前,我们需要获取certificate_arn的值,因为它是 ACM 为我们颁发证书的域生成的ARNAmazon 资源名称)。您可以从 ACM 仪表板中展开域部分获取 ARN 的值,如下截图所示:

zappa_settings.json.

文件—zappa_settings.json:

{
    "dev": {
        "app_function": "resources.api",
        "aws_region": "ap-south-1",
        "profile_name": "default",
        "project_name": "chapter-8",
        "runtime": "python3.6",
        "s3_bucket": "zappa-0edixmwpd",
        "remote_env": "s3://book-configs/chapter-7-config.json",
        "cache_cluster_enabled": false,
        "cache_cluster_size": 0.5,
        "cache_cluster_ttl": 300,
        "cache_cluster_encrypted": false,
        "events": [{
           "function": "schedulers.set_quote_of_the_day",
           "expression": "cron(0 12 * * ? *)"
       }],
       "domain": "quote.abdulwahid.info",
 "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784"
    }
}

在这里,我们将域配置为quote.abdulwahid.info并设置certificate_arn。现在,让我们使用zappa deploy <stage_name>命令部署应用程序,因为我们是第一次部署应用程序。看一下以下代码:

$ zappa deploy dev
Important! A new version of Zappa is available!
Upgrade with: pip install zappa --upgrade
Visit the project page on GitHub to see the latest changes: https://github.com/Miserlou/Zappa
Calling deploy for stage dev..
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter-7-dev-1529679507.zip (5.9MiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 6.17M/6.17M [00:02<00:00, 2.27MB/s]
Scheduling..
Scheduled chapter-7-dev-schedulers.set_quote_of_the_day with expression cron(0 12 * * ? *)!
Scheduled chapter-7-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Uploading chapter-7-dev-template-1529679513.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 4.76KB/s]
Waiting for stack chapter-7-dev to create (this can take a bit)..
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:09<00:00, 2.66s/res]
Deploying API Gateway..
Deployment complete!: https://5phr2bp4id.execute-api.ap-south-1.amazonaws.com/dev

如您所见,应用程序已部署在随机生成的 API 端点上。但是,为了配置应用程序,我们需要使用zappa certify命令将 API 网关与 ACM 证书关联起来,如下日志片段所示:

$ zappa certify
Calling certify for stage dev..
Are you sure you want to certify? [y/n] y
Certifying domain quote.abdulwahid.info..
Created a new domain name with supplied certificate. Please note that it can take up to 40 minutes for this domain to be created and propagated through AWS, but it requires no further work on your part.
Certificate updated!

一旦运行zappa certify命令,它将创建并将 API 网关与配置的证书关联起来。

现在,让我们再次更新部署,使用zappa update <stage_name>命令,如下所示。

$ zappa update dev
Important! A new version of Zappa is available!
Upgrade with: pip install zappa --upgrade
Visit the project page on GitHub to see the latest changes: https://github.com/Miserlou/Zappa
Calling update for stage dev..
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter-7-dev-1529679710.zip (5.9MiB)..
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 6.17M/6.17M [00:03<00:00, 863KB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading chapter-7-dev-template-1529679717.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 6.97KB/s]
Deploying API Gateway..
Scheduling..
Unscheduled chapter-7-dev-schedulers.set_quote_of_the_day.
Unscheduled chapter-7-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled chapter-7-dev-schedulers.set_quote_of_the_day with expression cron(0 12 * * ? *)!
Scheduled chapter-7-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://quote.abdulwahid.info (https://5phr2bp4id.execute-api.ap-south-1.amazonaws.com/dev)

就是这样。正如您所看到的,我们的应用程序现在在https://quote.abdulwahid.info上运行。现在,让我们在下一节中查看执行情况。

使用配置的域执行应用程序

我们已经在无服务器基础架构上部署和配置了我们的报价 API 应用程序。让我们使用 Postman API 客户端查看 API 执行。

每日报价 API

我们设计了这个 API(https://quote.abdulwahid.info/quote?type=daily)以每天返回一条报价。我们配置的调度程序将每天更新 UTC 时间表。看一下以下 cURL 日志片段:

$ curl https://quote.abdulwahid.info/quote?type=daily
{"quote": "Many wealthy people are little more than janitors of their possessions.", "author": "Frank Lloyd Wright", "category": "Famous"}

随机报价 API

随机报价 API(https://quote.abdulwahid.info/quote?type=random)将在每次请求时返回一条随机报价。看一下以下 cURL 日志片段:

$ curl https://quote.abdulwahid.info/quote?type=random
{"quote": "My mother thanks you. My father thanks you. My sister thanks you. And I thank you.", "author": "Yankee Doodle Dandy", "category": "Movies"}

就是这样。我们已成功在无服务器架构上部署了我们的应用程序。我们还配置了自定义域与我们的应用程序。这将用于测试目的。

总结

在本章中,我们学习了如何创建自定义域并配置域与 Route 53 集成。使用 Route 53,我们管理了域 DNS 配置。为了生成 SSL 证书,我们使用了 ACM,这很容易且直接。随后,我们使用生成的 ACM 证书的 ARN 配置了 Zappa 与域的集成。希望本章能帮助您了解为应用程序配置自定义域的机制。

现在我们要学习更多关于在 AWS Lambda 上安排任务和异步执行方法的知识。我们将进一步完善报价 API 应用程序,加入移动订阅模型。让我们为下一章做好准备,深入探讨使用 AWS Lambda 进行异步操作的世界。

问题

  1. AWS Route 53 是什么?

  2. 域名服务器是什么意思?

  3. ACM 如何保护在 AWS Lambda 上托管的 API?

第九章:AWS Lambda 上的异步任务执行

在本章中,我们将介绍 AWS Lambda 上的异步任务执行。AWS Lambda 使自动缩放和异步执行变得非常容易实现。Zappa 可以帮助我们配置任务,以便它们在 AWS Lambda 上以异步方式执行。Zappa 实现了管理异步任务响应的功能。

本章我们将涵盖以下主题:

  • 异步执行

  • 使用 Zappa 进行 AWS Lambda 异步调用

  • 使用异步功能配置 Quote API 应用程序

  • 使用 Zappa 部署和执行 Quote API

技术要求

在开始本章之前,请确保满足应用程序的先决条件。以下是您需要满足的技术要求:

  • Ubuntu 16.04/Windows/macOS

  • Python3.6

  • Pipenv 工具

  • 猎鹰框架

  • Zappa

  • 注册域名

  • AWS 账户

本章增强了第八章中开发的应用程序,带 SSL 的自定义域。因此,一些要求可以从先前配置的先决条件中使用。让我们继续学习 AWS Lambda 中的异步执行。

异步执行

异步执行在开发高性能和优化的应用程序中起着重要作用。AWS Lambda 支持异步执行。有不同的方法来以异步模式执行 AWS Lambda 函数。

理解异步执行

异步执行 是在不阻塞用户干预的情况下执行特定代码块的过程。为了更好地理解它,考虑 jQuery Ajax 机制发送异步请求到服务器,而不会阻塞用户,并在回调方法中捕获成功响应或错误响应。看一下以下图表,以更好地理解:

现在,您可以看到,一旦服务器从客户端获得异步请求,服务器立即返回确认的响应。一旦请求处理完成,将返回成功或失败的响应;否则,将不返回任何内容。

异步方法是否返回响应取决于要求。我们可能需要或不需要返回响应。如果我们希望返回响应,那么应该有一种机制来处理或捕获客户端端的响应。

类似地,AWS Lambda 函数可以以异步方式执行,这样我们就不会阻塞用户干预等待返回响应。有一些用例需要捕获以异步方式执行的 AWS Lambda 函数的响应。我们将在接下来的部分讨论捕获响应。

让我们看看 AWS Lambda 如何异步执行。

使用 Boto3 进行 AWS Lambda 异步执行

AWS Lambda 函数实际上就是云中的一个函数(一段代码)。函数可以同步或异步调用。为了在任何编程语言中实现异步执行,我们可以在单独的线程或进程中执行函数。例如,在 Python 中,有各种库可用于实现异步执行。同样,AWS Lambda 很好地支持异步执行。

让我们看一下以下代码片段:

client = boto3.client('lambda')
response = client.invoke(
    FunctionName='string',
    InvocationType='Event'|'RequestResponse'|'DryRun',
    LogType='None'|'Tail',
    ClientContext='string',
    Payload=b'bytes'|file,
    Qualifier='string'
)

我们可以使用 Boto3 库调用 AWS Lambda 函数。上面的代码是 Lambda 客户端 invoke 方法的语法。您可以在 Boto3 的官方文档中了解更多关于 invoke 方法机制的信息:boto3.readthedocs.io/en/latest/reference/services/lambda.html#Lambda.Client.invoke

Boto3 是一个组织良好、维护良好的 Python AWS SDK。它帮助开发人员使用 Python 与 AWS 服务进行交互。Zappa 也使用 Boto3 与 AWS 服务进行交互。

让我们简要解释一下InvocationType,它用于决定是以同步模式还是异步模式执行函数。如果您想以同步模式调用现有的 AWS Lambda 函数,那么可以选择InvocationTypeRequestResponse,对于异步模式,可以选择InvocationTypeEvent

以下代码是 Lambda 函数的异步执行示例:

client = boto3.client('lambda')
response = client.invoke(
    FunctionName='MyFunction',
    InvocationType='Event'
)

就是这样。这将以异步模式调用 Lambda 函数。借助 Boto3,您可以异步执行 AWS Lambda 函数。现在让我们看看如何使用 Zappa 执行异步执行。

AWS Lambda 使用 Zappa 进行异步调用

AWS Lambda 函数只是部署在 AWS Lambda 容器中的函数。因此,执行它只是调用一个函数。AWS 提供了各种调用方法。如何集成和配置调用以实现异步执行完全取决于您。我们已经在上一节中看到了如何使用 Boto3 SDK 进行异步执行。现在,我们将探索 Zappa 提供的各种调用方式。

使用任务装饰器进行异步 AWS Lambda 调用

Zappa 提供了一种超级简单的方式来配置 Lambda 执行为异步模式。Zappa 使用名为zappa.async.task的装饰器方法实现了异步执行。这个装饰器可以用于我们想要以异步模式执行的任何函数。以下是来自 Zappa 官方 GitHub 页面的示例(github.com/Miserlou/Zappa#asynchronous-task-execution):

from flask import Flask
from zappa.async import task
app = Flask(__name__)

@task
def make_pie():
    """ This takes a long time! """
    ingredients = get_ingredients()
    pie = bake(ingredients)
    deliver(pie)

@app.route('/api/order/pie')
def order_pie():
    """ This returns immediately! """
    make_pie()
    return "Your pie is being made!"

正如您所看到的,我们在make_pie方法上使用了task装饰器。现在,当您调用 API 时,它将立即返回响应,并以异步模式执行make_pie方法。以异步方式执行make_pie只是实例化具有make_pie方法执行上下文的 AWS Lambda 实例。这就是您可以异步执行函数的方式。现在,另一个挑战是收集异步执行函数的响应。我们将在接下来的部分讨论这个问题。

Amazon SNS 作为任务来源

Amazon Simple Notification ServiceSNS)是一种托管的发布/订阅消息服务。它支持各种协议,如 HTTP、HTTPS、电子邮件、电子邮件-JSON、Amazon SQS、应用程序、AWS Lambda 和短信。我们可以通过任何这些协议创建主题和订阅,尽管我们可以使用 AWS SNS 通过其 Web 控制台执行发布/订阅操作。

我们已经通过 API Gateway 调用了 AWS Lambda,这是我们所有实现的 API 都在工作的方式。同样,我们可以订阅我们的 AWS Lambda 与 Amazon SNS 的特定主题。现在,每当在该主题上发布任何消息时,它也会调用订阅的 AWS Lambda。

task_sns decorator binding:
from zappa.asycn import task_sns

@task_sns
def method_to_invoke_from_sns_event():
    pass

您还需要在zappa_settings.json文件中更新以下设置:

{
  "dev": {
    ..
      "async_source": "sns",
      "async_resources": true,
    ..
    }
}

当您调用zappa schedule命令时,它将自动创建并订阅 SNS。通过 SNS 主题发布的任何消息都会创建一个唯一的消息 ID。因此,您可以使用生成的消息 ID 在 CloudWatch 日志中跟踪消息响应。

此功能使您能够使用 Lambda 调用来执行基于 SNS 事件的操作。例如,您可以使用它来开发一个一次性密码OTP)生成应用程序,其中您不需要持久化 OTP 数据。相反,它将被发布到特定主题,订阅者将获得该信息。最后,AWS Lambda 和手机号码可以订阅 AWS SNS 主题。这将调用 AWS Lambda 方法,并使用 SNS 主题上发布的消息上下文。

让我们在下一节中看一下直接调用方法。

直接调用

Zappa 提供了另一种执行 Lambda 函数的直接调用的机制。以前,我们一直在使用tasktask_sns装饰器,但现在我们将使用zappa.async.run方法来执行直接调用。

zappa.async.run method being used:
from zappa.async import run

# Invoking a method in async mode using Lambda
run(method_name_to_invoke, args, kwargs)

# Invoking a method in async mode using SNS
run(method_name_to_invoke, args, kwargs, service="sns")

此功能将帮助您根据您的要求动态配置async调用。装饰器任务方法从编译中固定,但此方法语句可以在运行时有条件地调用。

远程调用

默认情况下,Zappa 执行当前 Lambda 实例的直接调用方法。但是,如果您希望在不同区域上将 Lambda 调用作为单独的 Lambda 函数执行,则可以更新您的任务装饰器,如下面的代码片段所示:

@task(remote_aws_lambda_function_name='subscribe-mobile-prod', remote_aws_region='us-east-1')
def subscribe_mobile_number(*args, **kwargs):
   """ This may take a long time! """
   validate(kwargs.get("mobile"))
   add_subscription(mobile=kwargs.get("mobile"))

我们正在使用task装饰器,但带有额外的参数,例如remote_aws_lambda_function_nameremote_aws_region。这些参数说明在特定区域执行特定 Lambda 函数。这就是您可以执行远程调用的方式。

让我们通过这些不同类型的调用来增强 Quote API 应用程序,以实现异步执行。

配置带有异步功能的 Quote API 应用程序

在上一章中,我们创建了一个 Quote API 并配置了自定义域。现在我们将增强和优化现有的应用程序。我们将添加一些新功能到应用程序中,以演示不同类型的调用。

我们将使用现有的代码库作为一个不同的项目,因此最好将现有的代码库复制到一个新目录中;在我们的情况下,我们将Chapter08代码库复制为Chapter09;但是,您需要更新zappa_settings.json文件。在即将到来的部分中,我们将介绍 Zappa 设置更改。

使用 Amazon SNS 进行每日报价的短信订阅

我们将添加每日接收报价的短信订阅的新功能。这将要求我们使用 Boto3 库配置 Amazon SNS。Boto3 是一个完整的 Python SDK 库,使我们能够以编程方式与 AWS 服务进行交互。让我们继续并在下一节中配置 Amazon SNS。

使用 Boto3 配置 Amazon SNS

您需要满足先决条件并遵循上一章中详细说明的安装说明,其中我们使用 Boto3 和其他所需的库配置了环境。假设您已经配置了环境,我现在将继续探索配置。

让我们来看一下以下代码片段:

client = boto3.client('sns',
            aws_access_key_id= os.environ['aws_access_key_id'],
            aws_secret_access_key= os.environ['aws_secret_access_key'],
            region_name= 'us-east-1')

正如您所看到的,我们正在使用 Boto3 创建 Amazon SNS 的客户端对象。我们需要访问密钥凭据以便以编程方式获取访问权限。

这是与 Amazon SNS 连接时的重要步骤。一旦成功创建了client对象,您可以执行各种操作,例如创建主题,使用协议订阅服务以及在主题上发布消息。

让我们朝着使用 Amazon SNS 实现短信订阅的实际实现迈进。

实现短信订阅功能

models.py class with along with OTPModel class:
import os
import datetime
from shutil import copyfile
from peewee import *

# Copy our working DB to /tmp..
db_name = 'quote_database.db'
src = os.path.abspath(db_name)
dst = "/tmp/{}".format(db_name)
copyfile(src, dst)

db = SqliteDatabase(dst)

class QuoteModel(Model):

    class Meta:
        database = db

    id = IntegerField(primary_key= True)
    quote = TextField()
    author = CharField()
    category = CharField()
    created_at = DateTimeField(default= datetime.date.today())

class OTPModel(Model):

    class Meta:
        database = db

    id = IntegerField(primary_key= True)
    mobile_number = CharField()
    otp = IntegerField()
    is_verified = BooleanField(default=False)
    created_at = DateTimeField(default= datetime.date.today())

db.connect()
db.create_tables([QuoteModel, OTPModel])
QuoteSubscription class.

文件-sns.py

import os
import re
import boto3

class QuoteSubscription:

    def __init__(self):
        """
        Class constructor to initialize the boto3 configuration with Amazon SNS.
        """
        self.client = boto3.client(
            'sns',
            aws_access_key_id=os.environ['aws_access_key_id'],
            aws_secret_access_key=os.environ['aws_secret_access_key'],
            region_name='us-east-1')
        topic = self.client.create_topic(Name="DailyQuoteSubscription")
        self.topic_arn = topic['TopicArn']

    def subscribe(self, mobile):
        """
        This method is used to subscribe a mobile number to the Amazon SNS topic.
        Required parameters:
            :param mobile: A mobile number along with country code.
            Syntax - <country_code><mobile_number>
            Example - 919028XXXXXX
        """
        assert(bool(re.match("^(\+\d{1,3}?)?\d{10}$", mobile))), 'Invalid mobile number'
        self.client.subscribe(
            TopicArn=self.topic_arn,
            Protocol='sms',
            Endpoint=mobile,
        )

    def unsubscribe(self, mobile):
        """
        This method is used to unsubscribe a mobile number from the Amazon SNS topic.
        Required parameters:
            :param mobile: A mobile number along with country code.
            Syntax - <country_code><mobile_number>
            Example - 919028XXXXXX
        """
        assert(bool(re.match("^(\+\d{1,3}?)?\d{10}$", mobile))), 'Invalid mobile number'
        try:
            subscriptions = self.client.list_subscriptions_by_topic(TopicArn=self.topic_arn)
            subscription = list(filter(lambda x: x['Endpoint']==mobile, subscriptions['Subscriptions']))[0]
            self.client.unsubscribe(
                SubscriptionArn= subscription['SubscriptionArn']
            )
        except IndexError:
            raise ValueError('Mobile {} is not subscribed.'.format(mobile))

    def publish(self, message):
        """
        This method is used to publish a quote message on Amazon SNS topic.
        Required parameters:
            :param message: string formated data.
        """
        self.client.publish(Message=message, TopicArn=self.topic_arn)

    def send_sms(self, mobile_number, message):
        """
        This method is used to send a SMS to a mobile number.
        Required parameters:
            :param mobile_number: string formated data.
            :param message: string formated data.
        """
        self.client.publish(
            PhoneNumber=mobile_number,
            Message=message
        )

这个类有一个用于执行移动号码订阅功能的方法。为了演示异步执行,我们将明确编写一些函数,这些函数将使用QuoteSubscription功能。

让我们创建一个名为async.py的文件,其中包含以下代码片段:

import random
from zappa.async import task
from sns import QuoteSubscription
from models import OTPModel

@task
def async_subscribe(mobile_number):
    quote_subscription = QuoteSubscription()
    quote_subscription.subscribe(mobile=mobile_number)

@task
def async_unsubscribe(mobile_number):
    quote_subscription = QuoteSubscription()
    quote_subscription.unsubscribe(mobile=mobile_number)

@task
def async_publish(message):
    quote_subscription = QuoteSubscription()
    quote_subscription.publish(message=message)

@task
def async_send_otp(mobile_number):
    otp = None
    quote_subscription = QuoteSubscription()
    data = OTPModel.select().where(OTPModel.mobile_number == mobile_number, OTPModel.is_verified == False)
    if data.exists():
        data = data.get()
        otp = data.otp
    else:
        otp = random.randint(1000,9999)
        OTPModel.create(**{'mobile_number': mobile_number, 'otp': otp})
    message = "One Time Password (OTP) is {} to verify the Daily Quote subscription.".format(otp)
    quote_subscription.send_sms(mobile_number=mobile_number, message=message)

正如您所看到的,我们定义了这些方法并添加了@task装饰器。在本地环境中,它将以正常方法执行,但在 AWS Lambda 上下文中,它将以异步模式执行。

让我们移动到资源 API 实现。我们将稍微修改现有资源。将会有一些与短信订阅相关的新 API。

文件-resources.py

import os
import re
import datetime
import requests
import falcon
import boto3

from models import QuoteModel, OTPModel
from mashape import fetch_quote
from async import async_subscribe, async_unsubscribe, async_send_otp

class DailyQuoteResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        try:
            data = QuoteModel.select().where(QuoteModel.created_at == datetime.date.today())
            if data.exists():
                data = data.get()
                resp.media = {'quote': data.quote, 'author': data.author, 'category': data.category}
            else:
                quote = fetch_quote()
                QuoteModel.create(**quote)
                resp.media = quote
        except Exception as e:
            raise falcon.HTTPError(falcon.HTTP_500, str(e))

class SubscribeQuoteResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        try:
            mobile_number = '+{}'.format(req.get_param('mobile'))
            otp = req.get_param('otp')
            otp_data = OTPModel.select().where(OTPModel.mobile_number == mobile_number, OTPModel.otp == otp, OTPModel.is_verified == False)
            if mobile_number and otp_data.exists():
                otp_data = otp_data.get()
                otp_data.is_verified = True
                otp_data.save()
                async_subscribe(mobile_number)
                resp.media = {"message": "Congratulations!!! You have successfully subscribed for daily famous quote."}
            elif mobile_number and not otp_data.exists():
                async_send_otp(mobile_number)
                resp.media = {"message": "An OTP verification has been sent on mobile {0}. To complete the subscription, Use OTP with this URL pattern https://quote-api.abdulwahid.info/subscribe?mobile={0}&otp=xxxx.".format(mobile_number)}
            else:
                raise falcon.HTTPError(falcon.HTTP_500, 'Require a valid mobile number as a query parameter. e.g https://<API_ENDPOINT>/subscribe?mobile=XXXXXXX')
        except Exception as e:
            raise falcon.HTTPError(falcon.HTTP_500, str(e))

class UnSubscribeQuoteResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        try:
            mobile_number = '+{}'.format(req.get_param('mobile'))
            if mobile_number:
                async_unsubscribe(mobile_number)
                resp.media = {"message": "You have successfully unsubscribed from daily famous quote. See you again."}
        except Exception as e:
            raise falcon.HTTPError(falcon.HTTP_500, str(e))

api = falcon.API()
api.add_route('/daily', DailyQuoteResource())
api.add_route('/subscribe', SubscribeQuoteResource())
api.add_route('/unsubscribe', UnSubscribeQuoteResource())

在这里,我们使用了资源类创建了一些 API,如前面的代码片段中所述。每个资源类代表一个单独的 API 端点。因此,我们创建了三个 API 端点,每个端点都有自己的工作流执行和用法。

让我们按照以下方式探索每个 API 端点的用法:

  • /daily:此 API 端点旨在返回每日报价数据。

  • /subscribe:此 API 端点旨在订阅任何手机号以获取每日报价短信。在订阅任何手机号之前,它实现了一种 OTP 验证。因此,它遵循执行订阅操作的 URL 模式。订阅需要两个步骤,例如生成订阅的 OTP,然后验证 OTP 以确认订阅。要生成订阅的 OTP,您需要使用带有mobile查询参数的 API,例如http://localhost:8000/subscribe?mobile=919028XXXX,要进行订阅确认,您需要使用带有mobileotp参数的 API,例如http://localhost:8000/subscribe?mobile=919028790411&otp=XXXX

  • /unsubscribe:此 API 端点旨在取消现有订阅的手机号。

API 查询参数已定义模式,因此您需要使用这些模式进行有效参数。对于 mobile 参数,您应该以<country_code><mobile_number>的格式发送手机号码。对于opt参数,您应该发送 4 位整数。

SubscribeQuoteResource and UnSubscribeQuoteResource classes are using async methods to perform the mobile number subscription and unsubscription operations. This would all be executed in asynchronous mode on AWS Lamda.

现在让我们继续部署应用程序,然后我们将了解其执行过程。

使用 Zappa 部署和执行 Quote API

部署是任何 Web 应用程序的重要部分。我们有幸拥有 Zappa 和 AWS Lambda,它们为我们提供了无服务器的本质。由于我们正在增强上一章中创建的 Quote API 应用程序,因此根据我们当前的需求,将进行一些修改。

在接下来的部分中,我们将讨论 Zappa 设置的一些更改。

设置虚拟环境

如前所述,我们正在使用Chapter08代码库。在zappa_settings.json文件中需要进行一些修改,例如将project_name更改为Chapter09,如下面的代码片段所示:

{
...
"project_name": "chapter-9"
...
}

一旦您更改了project_name,您需要使用pipenv install命令来配置虚拟环境。这将创建一个带有更改的project_name的新虚拟环境。

我们正在使用 Boto3 库与 Amazon SNS 进行交互。因此,我们还需要使用pipenv install boto3命令安装 Boto3。

设置环境变量

除了虚拟环境之外,我们还需要配置一些环境变量。我们正在使用 Mashape API(第三方 API 市场)和 Boto3 库。因此,我们将使用 Mashape API 密钥和我们的 AWS 访问凭据配置环境变量。

Zappa 提供了几种配置环境变量的机制。我们将使用"remote_env"。这种方法需要在 S3 存储桶上上传一个 JSON 文件。

以下是配置的 JSON 文件的代码片段:

{
    "Mashape_API_Endpoint" : "https://XXXXXXXXXXXXXX",
    "X_Mashape_Key": "XXXXXXXXXXXXXXXXXXXXXXXXX",
    "aws_access_key_id" : "XXXXXXXXXXXXX",
    "aws_secret_access_key" :"XXXXXXXXXXXXXXXXXXXXXXXXXXXx"
}

一旦您将此文件上传到 S3 存储桶,您可以将此文件的 S3 路径用作"remote_env"的值,如下面的代码片段所示:

{
...
"remote_env": "s3://book-configs/chapter-9-config.json",
...
}

Zappa 将根据此 JSON 文件自动设置环境变量。

AWS 和其他 API 凭据是机密和敏感数据;因此,您必须避免在公共 Git 存储库中提交这些数据。借助remove_env,您可以将凭据设置为 AWS Lambda 上的环境变量,并将其全部安全地保存在 S3 上。

添加具有 SSL 的自定义域

是时候为 Quote API 应用程序的增强版本配置特定的域了。Zappa 提供了一个名为domain的关键字,用于在文件设置中设置您的域名。

以下是配置域的代码片段:

{
    ...
    "domain": "quote-api.abdulwahid.info",
    ...
}

一旦您配置了域名,就需要使用 SSL 证书对其进行认证。我们已经使用Amazon Certificate Manager (ACM)生成了通配符 SSL 证书。因此,我们将使用相同的 ACM ARN,如下面的代码所示:

{
    ...
    "domain": "quote-api.abdulwahid.info",
    "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784"
    ...
}

现在您需要运行zappa certify命令,以创建子域并配置证书。看一下以下日志片段:

$ zappa certify
Calling certify for stage dev..
Are you sure you want to certify? [y/n] y
Certifying domain quote-api.abdulwahid.info..
Created a new domain name with supplied certificate. Please note that it can take up to 40 minutes for this domain to be created and propagated through AWS, but it requires no further work on your part.
Certificate updated!

如前面的日志片段所示,这个域名可能需要 40 分钟才能在 AWS 中创建和传播,但您无需再做任何工作。

让我们转到下一部分,在那里我们将为所有手机订阅者配置一个发布报价短信的事件。

安排事件发布短信

我们将每天向所有短信订阅者发送报价短信。短信订阅功能已经使用 Amazon SNS 和QuoteSubscription类实现。我们将在接下来的部分详细解释订阅工作流程。但在执行订阅之前,我们应该有一个配置和计划的事件,将在 SNS 主题上发布报价。

我们已经在QuoteSubscription构造函数中创建了 SNS 主题。此外,我们在async.py文件中编写了一个async方法async_publish。现在我们将使用这个方法异步发送报价消息。

为了保持模块化的代码库,我们创建了一个schedulers.py文件,将所有调度方法放在一个地方。

schedulers.py:
from models import QuoteModel
from mashape import fetch_quote
from sns import QuoteSubscription
from async import async_publish

def set_quote_of_the_day(event, context):
    QuoteModel.create(**fetch_quote())

def publish_quote_of_the_day(event, context):
    quote = fetch_quote()
    async_publish(message=quote['quote'])

正如我们在上一章中已经创建了一个调度方法set_quote_of_the_day,现在我们需要创建一个名为publish_quote_of_the_day的方法,负责在 Amazon SNS 主题上发布报价消息。

zappa_settings.json file:
{
    ...
    "events": [
       ...,
       {
 "function": "schedulers.publish_quote_of_the_day",
 "expression": "cron(0 12 * * ? *)"
 }],
    ...
}

我们配置了调度方法,使用cron表达式每天在 UTC 时间的凌晨 2:00 执行(协调世界时),这将是 IST 时间的上午 7:30(印度标准时间)。因此,印度的所有订阅者将在早晨收到短信。您可以根据自己的需求安排cron表达式。

当我们创建QuoteSubscription类的实例时,它会创建一个 SNS 主题,如下面的截图所示:

您的手机上可能已启用免打扰DND)。DND 适用于促销短信。因此,在这种情况下,您可以更改文本消息首选项部分中的默认消息类型,如下面的截图所示:

zappa_settings.json file:
{
    "dev": {
        "app_function": "resources.api",
        "aws_region": "ap-south-1",
        "profile_name": "default",
        "project_name": "chapter-9",
        "runtime": "python3.6",
        "s3_bucket": "zappa-0edixmwpd",
        "remote_env": "s3://book-configs/chapter-9-config.json",
        "cache_cluster_enabled": false,
        "cache_cluster_size": 0.5,
        "cache_cluster_ttl": 300,
        "cache_cluster_encrypted": false,
        "events": [{
           "function": "schedulers.set_quote_of_the_day",
           "expression": "cron(0 12 * * ? *)"
       },
       {
        "function": "schedulers.publish_quote_of_the_day",
        "expression": "cron(0 2 * * ? *)"
        }],
       "domain": "quote-api.abdulwahid.info",
       "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784"
    }
}

就这样,我们已经完成了配置域名与 Quote API 应用程序!现在我们将使用配置的域名来访问 API。

部署

Zappa 部署需要zappa_settings.json文件,该文件生成zappa init命令。但我们已经有了zappa_setttings.json文件,所以不需要再次运行此命令。

如果您是第一次部署应用程序,您需要使用zappa deploy <stage_name>,如果应用程序已经部署,则需要使用zappa update <stage_name>

zappa update command:
$ zappa update dev
Important! A new version of Zappa is available!
Upgrade with: pip install zappa --upgrade
Visit the project page on GitHub to see the latest changes: https://github.com/Miserlou/Zappa
Calling update for stage dev..
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter-9-dev-1528709561.zip (5.9MiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 6.17M/6.17M [00:02<00:00, 2.21MB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading chapter-9-dev-template-1528709612.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 17.0KB/s]
Deploying API Gateway..
Scheduling..
Unscheduled chapter-9-dev-schedulers.set_quote_of_the_day.
Unscheduled chapter-9-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled chapter-9-dev-schedulers.set_quote_of_the_day with expression cron(0 12 * * ? *)!
Scheduled chapter-9-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://quote-api.abdulwahid.info (https://5ldrsesbc4.execute-api.ap-south-1.amazonaws.com/dev)

哇!我们成功部署了 Quote API 应用程序!现在您可以看到配置的域名已经与 Quote API 应用程序一起运行。

让我们转到下一部分,在那里我们将看到 Quote API 应用程序的执行情况。

报价 API 执行

我们将使用curl命令行工具(curl.haxx.se/)。它使得从命令行与任何 HTTP/HTTPS 链接进行交互变得非常容易。(尽管开发人员更倾向于在编写 Shell 脚本时更多地使用它。)让我们看看每个 API 的执行情况。

每日报价 API

curl command execution:
$ curl https://quote-api.abdulwahid.info/daily
{"quote": "May the Force be with you.", "author": "Star Wars", "category": "Movies"}

每日报价短信订阅

我们已经集成了 Amazon SNS 来实现短信订阅功能。我们设计了 API /subscribe?mobile=<mobile_number>&otp=<otp_code> 用于在注册的手机上获取每日报价消息的订阅。

以下是日志片段,显示了订阅 API 的执行情况:

$ curl https://quote-api.abdulwahid.info/subscribe?mobile=919028XXXXXX
{"message": "An OTP verification has been sent on mobile +919028XXXXXX. To complete the subscription, Use OTP with this URL pattern https://quote-api.abdulwahid.info/subscribe?mobile=+919028XXXXXX&otp=XXXX."}

$ curl https://quote-api.abdulwahid.info/subscribe?mobile=919028XXXXXX&otp=XXXX
{"message": "Congratulations!!! You have successfully subscribed for daily famous quote."}

就是这样!我们已经安排了一个事件来发布每日报价消息到相关的 SNS 主题,这将广播到所有订阅。因此,订阅者现在将每天收到一条报价短信。一旦你调用这个 API,它就会创建一个 SNS 订阅。以下是亚马逊 SNS 网络控制台的屏幕截图:

你可以看到已经创建了一个订阅记录。现在在每条发布的消息上,这个订阅将接收到已发布的消息。

每日报价短信取消订阅

取消订阅 API 将负责移除任何已订阅的手机号码。这个 API 的工作流程类似于/subscribe API,使用了非常接近/subscribe?mobile=<mobile_number>的东西。

/unsubscribe API being executed:
$ curl https://quote-api.abdulwahid.info/unsubscribe?mobile=919028XXXXxx
{"message": "You have successfully unsubscribed from daily famous quote. See you again."}

这将从亚马逊 SNS 订阅中移除相关的订阅。以下是取消订阅 API 执行后亚马逊 SNS 网络控制台的屏幕截图:

你可以看到原来的订阅已经被移除了,所以我们已经实现了短信订阅/取消订阅以及异步执行机制。

摘要

在本章中,我们学习了关于异步工作流及其用法。我们还详细探讨了使用 Zappa 异步调用 AWS Lambda 函数。为了演示异步 Lambda 函数的执行,我们实现了报价 API 应用程序以及短信订阅功能。希望你喜欢本章,并且对你有所帮助!

现在我们将看到一些高级的 Zappa 配置,以便利用 Zappa 的自动化流程来维护应用程序部署的能力。让我们为下一章做好准备,开始你的新冒险之旅。

问题

  1. 什么是 AWS SNS?

  2. AWS Lambda 如何调用 SNS 主题?

第十章:高级 Zappa 设置

在本章中,我们将探索 Zappa 提供的各种设置和配置。这真的可以帮助您以高效的方式部署应用程序。因此,有各种设置来配置您的应用程序。这些设置与一些 AWS 服务及其功能和功能相关。我们将通过将它们应用到我们在第九章中开发的现有报价 API 应用程序来探索这些设置,AWS Lambda 上的异步任务执行

在本章中,我们将涵盖以下主题:

  • 保持服务器热

  • 启用 CORS

  • 处理更大的项目

  • 启用 bash 编译

  • 缓存未处理的异常

技术要求

在继续之前,有一些先决条件需要满足。为了满足先决条件,需要满足以下要求:

  • Ubuntu 16.04/Windows/macOS

  • Python 3.6

  • Pipenv 工具

  • Zappa

  • Falcon API

  • Python 包

  • 注册域名

  • AWS 账户

一些先前配置的先决条件可以从第九章中使用,AWS Lambda 上的异步任务执行。这意味着您可以继续使用配置的域和 AWS 服务。您可能需要更新本章的 Zappa 设置文件。

让我们继续探索与报价 API 应用程序一起使用的其他设置。

保持服务器热

Zappa 启用了一个保持 AWS Lambda 处于热状态的功能。由于容器化,AWS Lambda 存在冷启动,因此 Lambda 需要您设置环境以执行函数。每当 AWS Lambda 收到请求时,它会实例化 Lambda 函数及其所需的环境,最终在完成请求后销毁实例。

这就是 AWS Lambda 的工作原理。因此,Zappa 使用 AWS CloudWatch 事件调度功能来实现此机制,以保持实例化的 Lambda 实例处于热状态。保持 Lambda 处于热状态就是每四分钟触发 CloudWatch 事件作为 ping 请求,以防止 Lambda 实例的销毁。

此功能默认启用,但如果要禁用此功能,则可以在 Zappa 设置的 JSON 文件中将keep_warm标志设置为false

以下代码片段用于禁用保持热功能:

{
    "dev": {
             ...
             "keep_warm": true/false
             ...
     }
}

在我们的情况下,我们希望保持默认设置不变,以便我们的应用程序始终处于热状态。让我们继续下一节,我们将探索其他有用的设置。

启用 CORS

跨源资源共享CORS)是在相同域或不同托管域上提供 API 的重要部分。AWS API Gateway 提供了启用 CORS 功能的功能。一旦您在 API Gateway 上配置了 API 资源,您必须使用 API Gateway Web 控制台启用 CORS。在 API Gateway 资源上启用 CORS 需要您设置OPTION方法以及一些响应头,例如以下内容:

  • Access-Control-Allow-Methods

  • Access-Control-Allow-Headers

  • Access-Control-Allow-Origin

您可以查看 AWS 官方文档中有关在 API Gateway 中配置 CORS 的手动过程(docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html)。

Zappa 通过使用名为cors的设置属性自动配置 API Gateway 资源的 CORS 过程,如下面的代码片段所述:

{
    "dev": {
             ...
             "cors": true/false
             ...
     }
}

Zappa 将cors的默认值设置为false。如果要为 API 资源启用 CORS,则可以将其设置为true。它还支持添加响应头。

"cors": true"binary_support": true不兼容。因此,您可以禁用 API 网关级别的 CORS,或者您可以添加应用程序级别的 CORS 功能。

如前所述,您也可以使用应用程序级别的 CORS。有许多库可用于集成 CORS,一些框架有很好的库,比如django-cors-headers (github.com/ottoyiu/django-cors-headers) 和 Flask-CORS (github.com/corydolphin/flask-cors)。

这就是配置 CORS 功能的全部内容。我更喜欢在应用程序级别启用 CORS,因为这样您可以更好地控制它。

处理更大的项目

在这一部分,我们将讨论如何处理 AWS Lamda 上的大型项目的过程。AWS Lambda 默认支持不同的代码输入类型。现在,我们将更详细地讨论这个功能,因为我们将向您展示如何通过 AWS Lambda 控制台和使用 Zappa 库来处理这个功能。

使用 AWS Lambda 控制台处理更大的项目

AWS Lambda 支持三种不同的代码输入类型——内联编辑代码、上传 ZIP 文件和从 Amazon S3 上传文件,如下面的 AWS Lambda 函数 Web 控制台的截图所示:

这种输入类型允许用户将他们的代码库放在 AWS Lambda 上。让我们详细说明一下:

  • 使用这种输入类型,您可以通过 AWS Lambda 的 Web 控制台直接放置代码,就像前面的截图中提到的那样。借助其在线编辑器,您可以编写和执行代码。这可以用于小型代码库。

  • 上传 ZIP 文件:AWS Lambda 支持上传代码库的.zip 文件。我们在第一章中讨论了代码库的构建打包,Amazon Web Services for Serverless。这个功能有一个关于文件大小的限制,因为它只支持 50MB 大小的文件上传,但对于大型项目有另一个选项。

  • 从 Amazon S3 上传文件:这个功能允许用户将构建包上传到 Amazon S3 存储,无论大小。这意味着您可以通过其 S3 链接引用在 Amazon S3 上上传的构建包。

使用 Zappa 处理更大的项目

Zappa 在处理部署时考虑构建包大小。Zappa 只支持两种代码输入类型,即直接在 AWS Lambda 上上传.zip 文件和在 Amazon S3 上上传.zip 文件。

zappa_settings.json file:
{
    "dev": {
             ...
             "slim_handler": true/false
             ...
     }
}

如果项目大小超过 50MB,请将"slim_handler"设置为true。一旦设置了这个属性,Zappa 将自动将构建包上传到 Amazon S3 存储桶,并配置 AWS Lambda 处理程序函数以考虑来自 Amazon S3 存储桶的构建包。

启用 bash 标签编译

Bash 标签编译是命令行环境中的一个功能。通过按下Tab键,它将显示自动完成建议列表。Zappa 有许多命令,如果您将Zappa模块与 Python argcomplete模块注册,zappa命令将支持标签编译功能。

为了获得这个功能,您需要在系统或虚拟环境中安装argcomplete (github.com/kislyuk/argcomplete) 模块:

  • 系统级安装:
$ sudo apt update
$ sudo apt install python-argcomplete
  • 虚拟环境安装:
$ pip install argcomplete

一旦您配置了模块,那么您需要在全局级别激活 Python 的argcomplete模块。以下是激活全局 Python argcomplete模块的命令:

$ activate-global-python-argcomplete

为了将Zappa模块与argcomplete注册,您需要在~/.bashrc文件中添加以下行:

eval "$(register-python-argcomplete zappa)"

通过执行以下命令在当前终端上再次源化以立即生效:

$ source ~/.bashrc

现在,一旦您将Zappa模块与argcomplete注册,Zappa 命令将在编译中可用。以下是 Zappa 命令编译的截图:

这就是您可以使用argcomplete来进行 Zappa 命令。然而,在部署过程中更加高效将会很有帮助。让我们继续下一节,我们将讨论捕获未处理异常。

捕获未处理异常

Zappa 提供了一个捕获未处理异常的功能。这将允许您处理未处理的异常,通过电子邮件、SNS 或其他来源发出警报通知。这取决于您的要求,但您可以选择任何来源来发出通知。这将非常有帮助,这样您就可以跟踪任何部署环境中出现的故障。

例如,如果我们想要向所有开发人员和 QA 工程师发送关于任何部署环境的批量电子邮件通知,Zappa 提供了一种简单的方法来配置捕获未处理异常的机制。借助exception_handler属性的帮助,您可以绑定一个异常处理程序方法,从中可以处理异常以发送批量电子邮件通知。

以下是 Zappa 设置文件的代码片段:

{
    "dev": {
        ...
        "exception_handler": "your_module.unhandled_exceptions",
    },
    ...
}

在这里,异常处理程序是在一个模块中定义的方法。因此,让我们修改我们现有的项目,从第九章,在 AWS Lambda 上执行异步任务,添加异常处理程序。

unhandled_exception method that we created in the Quote API application of  Chapter 9, *Asynchronous Task Execution on AWS Lambda.*

文件-notify.py

import os
import boto3

def unhandled_exceptions(e, event, context):
    client = boto3.client('sns', aws_access_key_id=os.environ['aws_access_key_id'],
                            aws_secret_access_key=os.environ['aws_secret_access_key'],
                            region_name='us-east-1')
    topic = client.create_topic(Name="UnhandledException")
    client.publish(Message={'exception': e, 'event': event}, TopicArn=topic['TopicArn'])
    return True # Prevent invocation retry

在这里,我们将异常和事件数据发布到订阅的电子邮件中的"UnhandledException"主题。

我们可以增强订阅以管理开发人员和 QA 工程师的电子邮件订阅列表。这就是这个功能在追踪未处理异常方面的真正帮助。我们希望这对于管理您的部署是有益的。

总结

在本章中,我们了解了 Zappa 的一些附加功能。这些功能使我们能够以非常高效的方式管理 DevOps 操作。我们还探讨了处理大型项目、实现 CORS 和管理未处理异常。希望您喜欢本章,并在应用程序中开始使用这些功能。

问题

  1. 保持 AWS Lambda 处于热状态有什么用?

  2. CORS 是什么?

  3. 大型项目的部署流程是什么?

第十一章:使用 Zappa 保护无服务器应用程序

在本章中,我们将学习如何保护部署在 AWS Lambda 上的基于 Python 的应用程序。在之前的章节中,我们学习了如何开发一个应用程序并将其部署到无服务器基础设施上使用 Zappa。Zappa 还支持多种机制,使您能够为应用程序实现安全层。保护应用程序免受未经授权的访问是任何 Web 应用程序的重要过程,但能够在无服务器基础设施上保护 Web 应用程序将更加有趣。

因此,我们将开发一个基于 API 的应用程序,并演示一些机制来保护它免受未经授权的访问。让我们继续并探索有关设置开发环境的详细信息。

在本章中,我们将涵盖以下主题:

  • 实现随机引用 API

  • 在 API Gateway 上启用安全端点

  • 使用死信队列跟踪 AWS Lambda 的失败

  • 使用 AWS X-Ray 分析 Zappa 应用程序

  • 使用 AWS VPC 保护您的 Zappa 应用程序

技术要求

在本章中,我们将涵盖更多的 AWS 功能,以增强使用 Zappa 的安全层。在深入研究本章之前,请确保您已满足以下先决条件:

  • Ubuntu 16.04/macOS/Windows

  • Python 3.6

  • Pipenv 工具

  • AWS 账户

  • Gunicorn

  • Zappa

  • 其他 Python 包

一旦您启用了开发环境,我们就可以继续并开发一个简单的基于 Falcon 的 API,以便在请求时生成一个随机引用。在接下来的章节中,我们将使用 Zappa 使用不同的机制和方法来保护这个 API。

实现随机引用 API

在这一部分,我们将创建一个 RESTful API,用于生成随机引用。这将包括基于 Falcon 的 API 实现与 Mashape API 集成,就像我们在第九章中所做的那样,在 AWS Lambda 上执行异步任务。这一次,我们不打算集成数据库,因为我们不想保留任何信息。这将是一个简单的 HTTP GET 请求到我们的 API,然后我们将使用 Mashape API 返回一个随机生成的引用的 JSON 响应。让我们在下一节中看一下先决条件。

先决条件

我希望您已经满足了先前提到的技术要求,并使用 pipenv 工具设置了开发环境。现在,您需要在Mashape API 市场(market.mashape.com/)注册,我们将使用Random Famous Quote API(market.mashape.com/andruxnet/random-famous-quotes)。一旦您获得了使用此 API 的凭据,我们就需要在我们的应用程序中对其进行配置。

我们将使用 Zappa 的remote_env功能从 AWS S3 文件中将这些凭据作为环境变量共享,因此您需要在 AWS S3 上上传一个 JSON 文件。

文件—book-config/chapter-11-config.json

{
    "Mashape_API_Endpoint" : "https://andruxnet-random-famous-quotes.p.mashape.com/",
    "X_Mashape_Key": "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}

一旦您将此文件上传到 S3 存储中,您可以在zappa_settings.json文件中使用remote_env功能。以下是带有remote_env配置的zappa_settings.json的示例:

{
    "dev": {
        ...
        "remote_env": "s3://book-configs/chapter-11-config.json"
        ...
    }
}

一旦我们初始化 Zappa 进行部署,我们将添加这个设置。目前,您可以手动将这些凭据设置为环境变量,就像我们在这里所做的那样:

$ export Mashape_API_Endpoint=https://andruxnet-random-famous-quotes.p.mashape.com/
$ export X_Mashape_Key=XXXXXXXXXXXXXXXXXX

现在,让我们继续下一节,在那里我们将实现用于生成随机引用数据的 RESTful API。

开发随机引用 API

既然我们已经讨论了 Mashape API 的配置,让我们编写一个代码片段来实现获取随机引用数据的功能。请看以下代码片段:

文件—mashape.py

import os
import requests

def fetch_quote():
    response = requests.get(
        os.environ.get('Mashape_API_Endpoint'),
        headers={
            'X-Mashape-Key': os.environ.get('X_Mashape_Key'),
            'Accept': 'application/json'
        }
    )
    if response.status_code == 200:
        return response.json()[0]
    return response.json() 

正如您所看到的,我们编写了一个名为fetch_quote的方法,负责从 Mashape API 获取随机引用数据。我们将在进一步的实现中使用这个方法。

现在,让我们为我们的用户编写一个资源 API,他们将使用我们的 API 来获取随机引用。以下是资源 API 的代码片段。

文件-resource.py

import falcon
from mashape import fetch_quote

class RandomQuoteResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        try:
            resp.media = fetch_quote()
        except Exception as e:
            raise falcon.HTTPError(falcon.HTTP_500, str(e))

api = falcon.API()
api.add_route('/', RandomQuoteResource())

在这里,我们使用 Falcon 框架实现了一个 RESTful API。此 API 与根 URL 映射,即"/"。我们使用on_get方法仅接受 HTTP GET请求;其他请求将被拒绝访问。一旦用户发起GET请求,此 API 将返回随机引用数据。

你可以在本地环境上通过在本地主机上使用gunicorn运行此 API 来执行此 API:

$ gunicorn resources:api
[2018-07-11 13:59:28 +0530] [3562] [INFO] Starting gunicorn 19.9.0
[2018-07-11 13:59:28 +0530] [3562] [INFO] Listening at: http://127.0.0.1:8000 (3562)
[2018-07-11 13:59:28 +0530] [3562] [INFO] Using worker: sync
[2018-07-11 13:59:28 +0530] [3565] [INFO] Booting worker with pid: 3565

一旦运行gunicorn resources:api命令,API 将在本地使用8000端口可用。让我们使用curl命令执行 API:

$ curl http://localhost:8000
{"quote": "Whenever I climb I am followed by a dog called 'Ego'.", "author": "Friedrich Nietzsche", "category": "Famous"}

就是这样。我们已经完成了实施。现在,是时候使用 Zappa 在 AWS Lambda 上部署应用程序了。让我们继续下一节,我们将进一步讨论部署过程。

使用 Zappa 部署

要配置 Zappa,您应该运行zappa init命令并按照自动生成的问卷进行操作。我遵循了默认建议的设置,因此以下是自动生成的zappa_settings.json文件。

文件-zappa_settings.json

{
    "dev": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r"
    }
}

就是这样。现在,借助这个配置,您可以执行以下日志片段中提到的部署:

$ zappa deploy dev
Calling deploy for stage dev..
Creating chapter11-dev-ZappaLambdaExecutionRole IAM Role..
Creating zappa-permissions policy on chapter11-dev-ZappaLambdaExecutionRole IAM Role.
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter11-dev-1531293742.zip (5.6MiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5.92M/5.92M [00:02<00:00, 1.16MB/s]
Scheduling..
Scheduled chapter11-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Uploading chapter11-dev-template-1531293760.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 2.32KB/s]
Waiting for stack chapter11-dev to create (this can take a bit)..
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:09<00:00, 2.67s/res]
Deploying API Gateway..
Scheduling..
Unscheduled chapter11-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled chapter11-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://u1pao12esc.execute-api.ap-south-1.amazonaws.com/dev

在继续之前,让我们针对此应用程序集成一个自定义域。我们学习了如何使用 ACM 创建 SSL 证书并在第八章中配置自定义域,带 SSL 的自定义域。因此,我们将使用先前创建的通配符 SSL 证书。只需从 Zappa 设置中轻松创建新的自定义域。

我们将在zappa_settings.json文件中添加以下设置。

文件-zappa_settings.json

{
    "dev": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r",
        "remote_env": "s3://book-configs/chapter-11-config.json",
 "domain": "random-quote.abdulwahid.info",
 "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784"
    }
}
zappa update command:
$ zappa update dev
Calling update for stage dev..
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter11-dev-1531294072.zip (5.6MiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5.92M/5.92M [00:02<00:00, 2.19MB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading chapter11-dev-template-1531294078.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 8.55KB/s]
Deploying API Gateway..
Scheduling..
Unscheduled chapter11-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled chapter11-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://random-quote.abdulwahid.info (https://u1pao12esc.execute-api.ap-south-1.amazonaws.com/dev)

现在,应用程序已更新到 AWS Lambda,但我们仍然需要执行域认证任务以使域名上线。借助zappa certify命令,我们可以实现这一点。

zappa certify command:
$ zappa certify dev
Calling certify for stage dev..
Are you sure you want to certify? [y/n] y
Certifying domain random-quote.abdulwahid.info..
Created a new domain name with supplied certificate. Please note that it can take up to 40 minutes for this domain to be created and propagated through AWS, but it requires no further work on your part.
Certificate updated!

如前面的日志所述,我们的应用程序已经使用给定的自定义域名(random-quote.abdulwahid.info)上线,但可能需要长达 40 分钟才能创建域名并通过 AWS 传播,尽管这不需要您进一步的工作。让我们继续下一节,我们将执行已部署的应用程序。

执行 API

一旦应用程序上线,您可以使用 cURL 工具检查 API 的执行情况。以下是 API 执行的日志片段:

$ curl https://random-quote.abdulwahid.info
{"quote": "The significant problems we face cannot be solved at the same level of thinking we were at when we created them.", "author": "Albert Einstein", "category": "Famous"}

这就是全部关于无服务器的内容。现在,我们需要探索一些重要的步骤,以保护我们的应用程序免受未经授权的访问。让我们继续下一节,我们将讨论并实施一些解决方案来保护应用程序。

在 API Gateway 上启用安全端点

保护 API 访问是一个重要的标准。您可以限制和限制将要使用 API 的客户的访问。Amazon API Gateway 确实支持多种机制来保护、限制和限制 API 的使用。这将有助于根据您的客户群维护 API 的使用情况。以下是 API Gateway 支持的三种实现类型:

  • API 密钥

  • IAM 策略

  • API 网关 Lambda 授权者

让我们更详细地看看每个实现。

启用 API 密钥

正如我们在第一章中描述的,Amazon Web Services for Serverless,关于 Zappa 的部署工作流程,Zappa 配置 API 网关以使用代理传递机制调用 AWS Lambda,这在 API 网关接口上创建了一个 API。每个 API 都支持各种阶段。在我们的情况下,我们在部署应用程序时创建了一个dev阶段。因此,以下屏幕截图显示了 API 网关控制台的状态:

API Gateway 支持 API 密钥机制,您可以创建一个 API 密钥以及使用计划。借助这个 API 密钥,您可以限制客户的访问。任何客户都可以在 API 中设置x-api-key头与 API 密钥值来访问 API。API 密钥可以映射到任何 API 或阶段。

以下截图显示了创建 API 密钥的手动过程:

使用 Zappa 可以消除创建 API 密钥的手动过程。这就是 Zappa 发挥重要作用的地方,因为它将通过配置 Zappa 设置自动化整个过程。

Zappa 提供了一个布尔值的api_key_required属性。api_key_required默认设置为false,但如果您想生成 API 密钥,则需要将其设置为true。一旦将此属性设置为true,则需要重新部署应用程序。

api_key_required设置不适用于zappa update命令;它只适用于zappa deploy命令。因此,您需要取消部署应用程序,并从 Route 53 中删除已部署的自定义域的CNAME,然后从 API Gateway 控制台中删除自定义域。一旦删除了这些,就可以再次部署应用程序。

zappa_settings.json file with the "api_key_required" attribute.

文件-zappa_settings.json

{
    "dev": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r",
        "remote_env": "s3://book-configs/chapter-11-config.json",
        "domain": "random-quote.abdulwahid.info",
        "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784",
 "api_key_required": true
    }
}

现在,您可以再次使用zappa deploy命令执行新的部署,如下面的日志片段所示:

$ zappa deploy dev
Calling deploy for stage dev..
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter11-dev-1531334904.zip (5.6MiB)..
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5.92M/5.92M [00:12<00:00, 360KB/s]
Scheduling..
Scheduled chapter11-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Uploading chapter11-dev-template-1531334920.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.61K/1.61K [00:00<00:00, 10.4KB/s]
Waiting for stack chapter11-dev to create (this can take a bit)..
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:09<00:00, 4.69s/res]
Deploying API Gateway..
Created a new x-api-key: zp0snz9tik
Deployment complete!: https://laqdydyrg3.execute-api.ap-south-1.amazonaws.com/dev

请注意,Zappa 将生成新的x-api-key并返回 API 密钥 ID,如前面的日志片段所述。

部署完成后,您将能够在 API Gateway 控制台中看到自动生成的 API 密钥,如此处所示:

如前面的截图所示,您可以在 Zappa 设置中使用 API 密钥值,将 Zappa 部署的 API 与此密钥关联,以便 API 应用程序需要您在x-api-key头中具有此值。

下一步是通过单击前面截图中显示的Associated Usage Plans部分中的Add to Usage Plan将 API 密钥与使用计划关联起来。API 密钥可以与多个使用计划关联。这些计划使您能够根据您的业务模型为客户定义良好的结构使用计划。以下是第十一章 Basic Usage Plan 及其基本使用计划的截图:

如前面的截图所示,使用计划使您能够为每个 API 密钥定义限流限制和及时限定的 API 请求配额。一旦定义了计划,就可以将其与任何部署的 API 及其各自的阶段关联,如下面的截图所示:

我们将第十一章的Basic Usage Plan dev API 与dev阶段链接到了这个计划。这就是您可以为客户设置 API 的业务计划并共享 API 密钥以提供授权访问的方法。

现在,让我们在zappa_settings.json文件中使用前面 API 密钥截图中的 API 密钥值,并使用"api_key"属性。以下是更新后的zappa_settings.json文件。

文件-zappa_settings.json

{
    "dev": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r",
        "remote_env": "s3://book-configs/chapter-11-config.json",
        "domain": "random-quote.abdulwahid.info",
        "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784",
        "api_key_required": true,
"api_key":"yEddw9WeMH2UIZXHcaHQb1WvbindovrB55Rf4eAW"
    }
}

就这样。让我们再次使用zappa update命令更新部署,如下面提到的日志片段所示:

$ zappa update dev
Calling update for stage dev..
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter11-dev-1531337694.zip (5.6MiB)..
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5.92M/5.92M [00:16<00:00, 261KB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading chapter11-dev-template-1531337713.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.61K/1.61K [00:00<00:00, 8.50KB/s]
Deploying API Gateway..
Scheduling..
Unscheduled chapter11-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled chapter11-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://random-quote.abdulwahid.info (https://laqdydyrg3.execute-api.ap-south-1.amazonaws.com/dev)

我们已经完成了启用 API 密钥认证。让我们继续下一节,看看 API 的执行情况。

使用 API 密钥头执行 API

我们启用了 API 密钥的认证,因此 API 密钥与x-api-key头是强制的。如果请求没有x-api-key头,将被拒绝访问并返回禁止响应。如果用户在x-api-key头中提供有效的 API 密钥值,则将被允许访问 API 资源。

没有x-api-key头的 API 执行如下:

$ curl https://random-quote.abdulwahid.info/
{"message":"Forbidden"}

带有x-api-key头的 API 执行如下:

$ curl --header "x-api-key: yEddw9WeMH2UIZXHcaHQb1WvbindovrB55Rf4eAW" https://random-quote.abdulwahid.info/
{"quote": "Problems worthy of attack prove their worth by fighting back.", "author": "Paul Erdos", "category": "Famous"}

我们已经完成了 API 密钥身份验证集成。让我们继续下一节,我们将探索使用 IAM 策略进行身份验证的另一种选项。

IAM 策略

Amazon API Gateway 支持基于 IAM 的 V4 签名请求身份验证。API Gateway 要求用户通过对请求进行签名来进行身份验证。签署请求是使用加密函数创建数字签名的完整流程。您可以在以下链接中阅读有关签署请求过程的更多信息:

docs.aws.amazon.com/apigateway/api-reference/signing-requests/

Zappa 通过在 Zappa 的设置中将"iam_authorization"属性设置为true来启用此功能。此属性默认设置为false。因此,您可以显式将其设置为 true,以启用基于 IAM 的身份验证。此功能使您能够根据 IAM 策略访问 API 资源。您可以通过 IAM 策略(docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-iam-policy-examples.html)来控制此访问。

为了演示目的,我将为同一应用程序创建一个不同的阶段和自定义域。以下是 Zappa 设置的片段。

文件-zappa_settings.json:

{
    "dev": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r",
        "remote_env": "s3://book-configs/chapter-11-config.json",
        "domain": "random-quote.abdulwahid.info",
        "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784",
        "api_key_required": true,
        "api_key":"yEddw9WeMH2UIZXHcaHQb1WvbindovrB55Rf4eAW"
    },
    "dev_iam": {
 "app_function": "resources.api",
 "profile_name": "default",
 "project_name": "chapter11",
 "runtime": "python3.6",
 "s3_bucket": "zappa-ss0sm7k4r",
 "remote_env": "s3://book-configs/chapter-11-config.json",
 "domain": "random-quote-iam.abdulwahid.info",
 "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784",
 "iam_authorization": true
 }
}

在这里,我们创建了一个带有iam_authentication的不同阶段。此标志将启用基于 IAM 的身份验证。现在,您需要执行部署、更新和认证操作,以使此阶段与以下域名一起生效。

curl execution:
$ curl -s -w "\nStatus Code:%{http_code}" https://random-quote-iam.abdulwahid.info
{"message":"Missing Authentication Token"}
Status Code:403

现在,您需要对请求进行签名以访问部署的资源。签署请求需要您遵循一些流程,如此处所述:docs.aws.amazon.com/apigateway/api-reference/signing-requests/。还有许多第三方库可用于生成签署请求所需的标头。我们将使用requests-aws-sign(github.com/jmenga/requests-aws-sign)库来使用签名请求访问 API 资源。

以下是签署请求以访问 API 资源的代码片段。

文件-aws_sign_request_test.py:

import os
import requests
from requests_aws_sign import AWSV4Sign
from boto3 import session

# You must provide a credentials object as per http://boto3.readthedocs.io/en/latest/guide/configuration.html#configuring-credentials
# This example attempts to get credentials based upon the local environment
# e.g. Environment Variables, assume role profiles, EC2 Instance IAM profiles
session = session.Session(
    aws_access_key_id=os.environ['aws_access_key_id'],
    aws_secret_access_key=os.environ['aws_secret_access_key'])
credentials = session.get_credentials()

# You must provide an AWS region
region = session.region_name or 'ap-south-1'

# You must provide the AWS service. E.g. 'es' for Elasticsearch, 's3' for S3, etc.
service = 'execute-api'

url = "https://random-quote-iam.abdulwahid.info/"
auth=AWSV4Sign(credentials, region, service)
response = requests.get(url, auth=auth)

print (response.content)

就是这样!现在,您可以看到前面脚本的输出,如下面的代码所示:

$ python aws_sign_request_test.py 
b'{"quote": "Many wealthy people are little more than janitors of their possessions.", "author": "Frank Lloyd Wright", "category": "Famous"}'

最后,我们通过签名请求获得了 API 访问权限。通过这种方式,您可以使用 IAM 身份验证保护无服务器 API 应用程序。让我们继续下一节,我们将探索保护无服务器 API 应用程序的另一种方式。

API Gateway Lambda 授权程序

Amazon API Gateway Lambda 授权程序是一个简单的 AWS Lambda 函数,作为授权程序来控制对 API Gateway 资源的访问。这是因为 Lambda 授权程序将负责通过 bearer token 形式的授权头验证请求,并返回有效的 IAM 策略。您可以根据JWT(JSON Web Token)、OAuth 或 SAML 编写自定义的 Lambda 授权程序,具有不同的身份验证策略。

您可以从 API Gateway 控制台添加授权程序,如官方 AWS 文档中所述(docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html),或者您可以从名为api-gateway-authorizer-python的 Lambda 蓝图创建 Lambda 授权程序(github.com/awslabs/aws-apigateway-lambda-authorizer-blueprints/blob/master/blueprints/python/api-gateway-authorizer-python.py),然后从 API Gateway 控制台将此 Lambda 函数关联为 API 资源的授权程序。

一旦您配置了授权者,API 网关期望请求以及授权头中的持有者令牌或参数。如果缺少授权头,它将拒绝请求。如果客户端向您的 API 资源发送带有持有者令牌的请求授权头,那么 API 网关将从请求头中提取持有者令牌和其他参数,并将它们作为事件参数提供给 Lambda 授权者函数。Lambda 授权者使用现有的 AWS IAM 策略或 AWS Cognito 用户池验证令牌,然后返回 IAM 策略以授权请求。API 网关通过在预配置的TTL生存时间)期间缓存请求令牌的返回策略来维护子请求的会话,从 300 到 3600 秒, 默认为 300 秒。

Zappa 支持一种更简单的方法来配置 Lambda 授权者。您可以在 Zappa 设置中定义授权者属性如下:

{
    "dev" : {
        ...
        "authorizer": {
            "function": "your_module.your_auth_function", 
            "arn": "arn:aws:lambda:<region>:<account_id>:function:<function_name>",
            "result_ttl": 300,
            "token_header": "Authorization", // Optional. Default 'Authorization'. The name of a custom authorization header containing the token that clients submit as part of their requests.
            "validation_expression": "^Bearer \\w+$", // Optional. A validation expression for the incoming token, specify a regular expression.
        }
        ...
    }
}

我们可以定义前面的属性。每个属性都有其自己的特定用途,以定义自定义 Lambda 授权者。让我们更详细地探讨这些属性:

  • function:这将是您自己的本地函数,用于执行令牌验证。Zappa 将自动创建并映射此函数作为 API 网关中的授权者。

  • arn:这将是您现有 Lambda 函数的arn,用于验证令牌。如果您选择蓝图 Lambda 授权者函数api-gateway-authorizer-pythongithub.com/awslabs/aws-apigateway-lambda-authorizer-blueprints/blob/master/blueprints/python/api-gateway-authorizer-python.py),那么您可以放入由蓝图创建的 Lambda 函数的arn

  • result_ttl:这是一个可选属性。它通过 API 网关启用生存时间TTL)周期来缓存授权者结果。默认情况下,它设置为 300 秒,您可以将其设置为最多 3600 秒。

  • token_header:这是一个可选属性。它用于设置自定义授权头的名称。它包含客户端提交的请求的一部分令牌。

  • validation_expression:这是一个可选属性。它用于设置授权头中令牌的验证表达式。默认情况下,它支持"^Bearer \\w+$"表达式来验证令牌表达式。

这是您可以为无服务器 API 创建自定义 Lambda 授权者的方法。这使您能够为 Zappa 部署的所有分布式 API 微服务创建集中式身份验证。

现在,让我们继续下一节,我们将探讨 AWS 失败的跟踪机制。

使用死信队列跟踪 AWS Lambda 失败

死信队列DLQ)是亚马逊定义的机制,用于跟踪 AWS Lambda 函数在异步执行时的失败。AWS Lambda 在事件被丢弃之前,会以异步模式调用并在失败的情况下重试两次。DLQ 用于将此失败事件传递到 Amazon SQS 队列或 Amazon SNS 主题。

手动 DLQ 配置

DLQ 可以通过在 Lambda 函数的DeadLetterConfig参数上设置TargetArn(即 SQS 队列 ARN 或 SNS 主题 ARN)来进行配置,如下所述:

{
    "Code": {
        "ZipFile": blob,
        "S3Bucket": “string”,
        "S3Key": “string”,
        "S3ObjectVersion": “string”
    },
    "Description": "string",
    "FunctionName": "string",
    "Handler": "string",
    "MemorySize": number,
    "Role": "string",
    "Runtime": "string",
    "Timeout": number
    "Publish": bool,
    "DeadLetterConfig": {
 "TargetArn": "string" 
 }
} 

使用 Zappa 自动化 DLQ 配置

为了自动化这个过程,Zappa 通过将 SQS 队列/SNS 主题 ARN 值设置为dead_letter_arn来启用这个功能。我们在第九章中创建了一个名为UnhandledException的 SNS 主题,让我们使用现有的 SNS 主题,它已经订阅了我的电子邮件。只有在异步 Lambda 函数调用失败和重试时,DQL 才会触发。然后,DQL 将把故障异常处理为消息发送到配置的 SNS 主题,我们将在订阅的电子邮件上收到处理后的异常数据。

现在,以下代码片段是更新后的 Zappa 设置。

文件-zappa_settings.json

{
    "dev": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r",
        "remote_env": "s3://book-configs/chapter-11-config.json",
        "domain": "random-quote.abdulwahid.info",
        "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784",
        "api_key_required": true,
        "api_key":"yEddw9WeMH2UIZXHcaHQb1WvbindovrB55Rf4eAW",
        "dead_letter_arn": "arn:aws:sns:ap-south-1:042373950390:UnhandledException"
    },
    "dev_iam": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r",
        "remote_env": "s3://book-configs/chapter-11-config.json",
        "domain": "random-quote-iam.abdulwahid.info",
        "certificate_arn":"arn:aws:acm:us-east-1:042373950390:certificate/af0796fa-3a46-49ae-97d8-90a6b5ff6784",
        "iam_authorization": true
    }
}

在这里,我只为dev阶段更新了dead_letter_arn属性。因此,这个功能将在dev阶段可用。现在,我们已经在dev阶段的 Lambda 函数中设置好了 DLQ。完成配置后,您需要使用zappa deploy命令进行部署。就是这样!现在,我们的代码中应该有一个异步的 Lambda 函数机制,在运行时引发异常。

请注意,对于特定 Lambda 函数的更改,您需要使用zappa deploy命令重新部署函数。zappa update命令在这里不起作用,因为它负责更新现有的代码库,而不是 Lambda 配置。

在异步 Lambda 函数中引发异常

为了在异步 Lambda 调用中引发异常,我们需要有一个机制来实例化一个异步 Lambda 函数。让我们编写一个资源 API 并调用一个异步任务,这将引发异常。

以下是resources.py的更新代码:

import falcon
from zappa.async import task
from mashape import fetch_quote

class RandomQuoteResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        try:
            resp.media = fetch_quote()
        except Exception as e:
            raise falcon.HTTPError(falcon.HTTP_500, str(e))

@task
def async_task():
    raise ValueError("Async Failure Exception")

class AsyncTaskResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        try:
            async_task()
            resp.media = 'Called async task'
        except Exception as e:
            raise falcon.HTTPError(falcon.HTTP_500, strsk(e))

api = falcon.API()
api.add_route('/', RandomQuoteResource())
api.add_route('/async-failure', AsyncTaskResource())

在这里,我们创建了一个AsyncTaskResource作为"/async-failure"路由的资源类。这个路由使用AsyncTaskResource类中的on_get方法定义了 HTTP GET请求。我们还使用任务装饰器将async_task方法创建为一个异步方法。我们已经在第九章中看到了使用 Zappa 执行异步任务的实现,任务装饰器将在单独的 Lambda 实例中异步执行该方法。

async_task中,我们引发了一个ValueError异常。这将导致异步 Lambda 执行失败,并在随后的失败时引发 DLQ 事件。DLQ 将把异常数据处理到我们配置的 SNS 主题 ARN。最后,我们将在电子邮件中收到异常信息。

async-failure API:
$ curl -H "x-api-key: yEddw9WeMH2UIZXHcaHQb1WvbindovrB55Rf4eAW" https://random-quote.abdulwahid.info/async-failure
"Called async task"

我们请求了/async-failure API,它立即响应并在异步 Lambda 函数中实例化了任务。由于我们在async_task方法中明确引发了异常,这将调用 DLQ 并通过发布到 SNS 主题来处理异常信息。以下是从 AWS 通知消息收到的电子邮件通知的截图:

这样,我们就可以追踪未知的故障。这个功能将帮助我们提高应用程序的质量并减少故障率。让我们继续下一节,我们将探讨如何使用 AWS X-Ray 分析 Zappa 应用程序。

使用 AWS X-Ray 分析 Zappa 应用程序

AWS X-Ray 是亚马逊网络服务提供的分析服务。它帮助开发人员对应用程序的行为和工作流程进行分析。借助 X-Ray,开发人员可以了解应用程序的性能并追踪根本原因以改进优化。

AWS X-Ray 可以在任何计算 AWS 服务上启用。一旦您启用了 X-Ray,它就会根据应用程序的交互生成数据段。例如,如果您向应用程序发出 HTTP 请求,那么 X-Ray 将生成有关主机、请求、响应、计算时间和错误的数据。基于这些数据段,X-Ray 生成了一个服务图。

服务图提供了一个可视化模式,供开发人员了解应用程序工作流程并帮助确定其性能。除了请求和响应数据生成外,X-Ray 还为您的应用程序与 AWS 资源、微服务、数据库和 HTTP Web API 调用的交互生成记录。

AWS Lambda 的 X-Ray 手动集成

AWS Lambda 控制台具有特权,可以配置 Lambda 函数与 AWS X-Ray。因此,任何与 AWS Lambda 的交互都将被 AWS X-Ray 记录。您可以通过从控制台页面配置函数来在 Lambda 函数上启用 X-Ray,如下面的截图所示:

关于 AWS Lambda 控制台工作流程,您需要选择 AWS XRay。然后,您可以从主部分的底部面板配置其关联的设置,如下面的截图所示:

一旦选择了 X-Ray,默认执行角色权限将附加到您的 Lambda 函数。这样,AWS X-Ray 将记录对 Lambda 函数 API 执行的跟踪。

Zappa 配置以启用 AWS X-Ray 支持

Zappa 始终在这里,以避免手动交互来配置您的 Lambda 函数。因此,Zappa 提供了一种简单的方法来配置 AWS X-Ray 与您的 Lambda 函数。您只需在 Zappa 设置中将"xray_tracing"设置为true。这将自动为您的 Lambda 函数启用 X-Ray 跟踪支持。

让我们创建现有 API 应用的另一个阶段。这个阶段将具有基本配置,没有身份验证和自定义域,因为我们只是想演示 X-Ray 的工作流程。以下是具有 X-Ray 支持的新阶段配置。

文件 - zappa_settings.json

{
    ...
    "dev_xray": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r",
        "remote_env": "s3://book-configs/chapter-11-config.json",
        "xray_tracing": true
    }
}

如前所述,我们已经添加了一个名为"dev_xray"的新阶段,具有基本配置和 AWS X-Ray 跟踪支持。现在,让我们使用zappa deploy命令部署这个阶段。

deploy command:
$ zappa deploy dev_xray
Calling deploy for stage dev_xray..
Creating chapter11-dev-xray-ZappaLambdaExecutionRole IAM Role..
Creating zappa-permissions policy on chapter11-dev-xray-ZappaLambdaExecutionRole IAM Role.
Downloading and installing dependencies..
 - lazy-object-proxy==1.3.1: Using locally cached manylinux wheel
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter11-dev-xray-1531691356.zip (8.2MiB)..
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8.65M/8.65M [00:19<00:00, 460KB/s]
Scheduling..
Scheduled chapter11-dev-xray-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Uploading chapter11-dev-xray-template-1531691381.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.64K/1.64K [00:00<00:00, 9.68KB/s]
Waiting for stack chapter11-dev-xray to create (this can take a bit)..
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:09<00:00, 4.70s/res]
Deploying API Gateway..
Deployment complete!: https://r0wagu3zh3.execute-api.ap-south-1.amazonaws.com/dev_xray

就是这样!现在,我们的随机引用 API 已经在不同的阶段上线运行。一旦应用程序部署,Zappa 将生成一个随机的 API Gateway 链接,如前面的日志片段中所述。现在,您可以使用 curl 工具来访问 API。

以下是 API 执行的日志片段:

$ curl https://r0wagu3zh3.execute-api.ap-south-1.amazonaws.com/dev_xray
{"quote": "A lie gets halfway around the world before the truth has a chance to get its pants on.", "author": "Sir Winston Churchill", "category": "Famous"}

我们已经集成了 AWS X-Ray,因此我们应用程序的所有交互将被 AWS X-Ray 记录为跟踪段。以下是 AWS X-Ray 服务地图的截图:

在这里,您可以查看应用程序的跟踪详细信息。这些详细信息根据其控制台上的时间范围可用。AWS X-Ray 支持客户端 SDK 库,使开发人员能够根据其需求持久化这些跟踪。AWS X-Ray 的客户端 SDK 具有许多实现,涵盖了多种语言和特定语言的框架。您可以在以下链接了解有关 AWS X-Ray 及其基于 Python 的 SDK 库的更多信息:

docs.aws.amazon.com/xray/latest/devguide/aws-xray.html

github.com/aws/aws-xray-sdk-python

让我们继续下一节,我们将探讨 AWS VPC 与 AWS Lambda 函数的集成。

使用 AWS VPC 保护您的 Zappa 应用程序

AWS 虚拟私有云VPC)是专门为 AWS 资源提供的隔离虚拟网络服务。它类似于您自己数据中心中的传统网络机制。AWS VPC 使您能够保护 AWS 资源免受未经授权的访问。AWS 为每个区域提供了默认 VPC。默认 VPC 可帮助您配置所有 AWS 资源。

AWS VPC 专门为您的 AWS 账户提供了隔离层。您可以使用 AWS VPC 配置您的 AWS 资源。启用 VPC 后,您可以根据需要指定以下组件,例如 IP 地址范围、子网、安全组、路由表等。这些组件用于设置网络策略和策略。

VPC 的手动配置

AWS Lambda 有配置 VPC 的特权。以下是 AWS Lambda 配置的屏幕截图:

如前面的屏幕截图所示,我们选择了默认 VPC。我们需要配置其他组件,如子网和安全组,这是必需的。子网是 VPC 中的 IP 地址范围。对于需要互联网访问的任何资源,应使用公共子网。私有子网用于不需要连接到互联网的任何资源。

另一方面,安全组定义了授权任何协议访问的入站和出站规则。

AWS VPC 具有完整的安全网络层实现。要了解 VPC 概念的每个方面,您应该阅读其官方文档(docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Introduction.html)。我们将专注于 Zappa 配置,以便以自动化方式启用 VPC。让我们继续下一节,在那里我们将配置 Zappa 与 VPC。

使用 Zappa 进行 VPC 配置

Zappa 有一种优化的方式来自动化部署应用程序的 VPC。您只需要提供vpc_config属性和子网和安全组 ID,如此处所述:

{
    ...
    "vpc_config": { 
        "SubnetIds": [ "subnet-12345678" ],
        "SecurityGroupIds": [ "sg-12345678" ]
    },
    ...
}

我在前一节中提到了默认 VPC。您可以从 VPC 仪表板页面获取默认子网 ID,如下面的屏幕截图所示:

您可以通过从左侧面板选择安全组来获取安全组 ID,如下面的屏幕截图所示:

现在,我们将创建另一个带有 VPC 配置的部署阶段。您需要从前面的屏幕截图中放入子网 ID 和安全组 ID,并使用 Zappa 设置进行配置,如下面的代码片段所示。

文件—zappa_settings.json

{
    ...,
    "dev_vpc": {
        "app_function": "resources.api",
        "profile_name": "default",
        "project_name": "chapter11",
        "runtime": "python3.6",
        "s3_bucket": "zappa-ss0sm7k4r",
        "remote_env": "s3://book-configs/chapter-11-config.json",
        "vpc_config": {
 "SubnetIds": [ "subnet-1b10a072", "subnet-6303f22e" ],
 "SecurityGroupIds": [ "sg-892c4be0" ]
 }
    }
}

AWS VPC 是一个隔离的网络,因此在 VPC 网络内运行的任何服务都无法访问公共互联网。如果需要为任何资源访问公共互联网,则必须至少有两个子网。在 VPC 仪表板中使用以下设置:

  • 对于subnet-a

选择 NAT 网关部分并创建一个 NAT 网关。

选择 Internet 网关部分并创建一个 Internet 网关。

从路由表部分,创建一个名为route-a的路由,将 Internet 网关指向0.0.0.0/0

  • 对于subnet-b

使用此子网配置您的 Lambda 函数。

从路由表部分,创建一个名为route-b的路由,将属于subnet-a的 NAT 指向0.0.0.0/0

zappa deploy command:
$ zappa deploy dev_vpc
Important! A new version of Zappa is available!
Upgrade with: pip install zappa --upgrade
Visit the project page on GitHub to see the latest changes: https://github.com/Miserlou/Zappa
Calling deploy for stage dev_vpc..
Downloading and installing dependencies..
 - lazy-object-proxy==1.3.1: Downloading
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 56.0K/56.0K [00:00<00:00, 4.88MB/s]
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter11-dev-vpc-1532712120.zip (8.2MiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8.65M/8.65M [00:03<00:00, 2.56MB/s]
Scheduling..
Scheduled chapter11-dev-vpc-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Uploading chapter11-dev-vpc-template-1532712136.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.64K/1.64K [00:00<00:00, 40.8KB/s]
Waiting for stack chapter11-dev-vpc to create (this can take a bit)..
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:09<00:00, 2.38s/res]
Deploying API Gateway..
Deployment complete!: https://6odti0061c.execute-api.ap-south-1.amazonaws.com/dev_vpc

就是这样。现在,我们的应用已成功配置了 AWS VPC。

摘要

在本章中,我们学习了不同的安全机制,并演示了它们在一个小型基于 API 的应用程序中的实现。AWS 拥有非常好的安全架构,但涉及手动交互过程,而 Zappa 自动化了这些过程并防止了手动交互。我们还涵盖了优化应用程序工作流程的跟踪、分析和通知过程。

在下一章中,我们将探讨 Zappa 开发,以及 Docker 容器化。请继续关注,以便您可以提升新的技能。

问题

  1. 什么是 API 网关授权者?

  2. 什么是 AWS Lambda DQL?

  3. 为什么 AWS VPC 很重要?

第十二章:使用 Docker 的 Zappa

在本章中,我们将学习如何在 AWS Lambda 环境或操作系统上下文中开发无服务器应用,而不是在本地开发环境中。我们将专注于不同环境上出现的问题,并寻找这些问题的有效解决方案。

本章我们将涵盖以下主题:

  • 理解 Docker

  • 问题陈述

  • 具有自定义工具依赖的 API 开发

  • 使用 Docker 构建、测试和部署

技术要求

在继续之前,让我们配置一些先决条件,例如我们将需要的工具和软件包,以便设置开发环境。以下是您将需要的软件和软件包的列表:

  • Ubuntu 16.04/macOS/Windows

  • Docker

  • Python 3.6

  • Pipenv 工具

  • Falcon

  • Falcon-multipart

  • Gunicorn

  • catdoc

  • Zappa

在这里,我们提到了操作系统以及其他所需的工具和软件包。选择其中任何一个操作系统,并根据其官方网站上详细的说明安装 Docker(docs.docker.com/)。我们将在即将到来的 API 开发部分看到有关安装特定于 Python 的软件包的详细信息。让我们转到下一节,我们将了解 Docker 的概念。

理解 Docker

Docker是一个用于开发和部署应用程序的容器平台。Docker 基于 Docker 镜像创建这些容器,Docker 镜像包括类似于 Linux 的基本和必需组件。Docker 容器只不过是 Docker 镜像的一个实例。

Docker 容器具有许多功能,支持和运行任何应用程序。Docker 容器轻巧、灵活、便携、可扩展和可堆叠。您可以为任何服务创建一个容器,例如 Python 或 MySQL。Docker 使您能够通过与主机机器进行网络连接来共享数据。借助 Docker 容器,您可以为应用程序创建一个隔离的环境。

您可以创建自己的 Docker 镜像,其中包含堆叠的服务和配置。例如,您可以使用基于 Ubuntu 的镜像容器,然后安装 MySQL 服务,然后相应地配置容器。然后,我们可以构建一个包含配置服务的新镜像。最后,我们可以将镜像推送到 Docker hub 存储库(hub.docker.com/),这取决于我们的存储库权限,我们可以将其保持私有或公开。

您可以在其官方网站docs.docker.com/get-started/#docker-concepts上详细阅读并了解 Docker 技术的概念。我们将专注于使用环境相关软件包开发应用程序,并在 AWS Lambda 上部署。让我们转到下一节,我们将讨论在开发级别上维护 AWS Lambda 环境的真实情况。

问题陈述

尽管 Zappa 会处理您安装的 Python 软件包,并使用预编译的 Lambda 软件包(github.com/Miserlou/lambda-packages)和来自虚拟环境的 wheels 在 Lambda 上部署它们,但这些软件包可能会因操作系统环境而异。因此,可能会出现需要特定于操作系统的工具或自定义软件包来实现解决方案的情况。这种软件包上下文可能会根据操作系统环境而异。因此,在 AWS Lambda 环境中可能无法工作。

为了克服不同的环境上下文问题,并根据 AWS Lambda 环境维护安装的软件包,我们需要一个类似的开发环境上下文。因此,我们需要一个 Docker 镜像,它具有与 AWS Lambda 相似的上下文和环境。最后,LambCI (github.com/lambci) 开发了一个Docker-Lambda (github.com/lambci/docker-lambda) 镜像,它具有与 AWS Lambda 相同的上下文,包括系统库、文件结构、用户和权限、环境变量和其他上下文信息。

建议的解决方案

catdoc command execution:
$ catdoc sample-doc-file.doc 
This is a sample text for demo.

现在,这个catdoc命令已经在操作系统级别安装了。但在我们的 API 中,我们将以编程方式执行这个命令,以解析和获取来自stdout的打印文本数据。我们的 API 可以作为一个解析器服务,从 Microsoft Office 2003 格式文件中获取文本数据。

这个解决方案的案例研究

我选择这个问题是因为目前很少有 Python 库可以解析 Doc 格式的文件。我在组织中开发了一个应用程序,需要解析所有类型的文本文件,如.pdf.txt.docx.doc。因此,我遇到了这样的情况,我必须使用一个依赖于操作系统的命令行工具来以编程方式获取文本数据。我开发了一个解决方案,在我的本地 Ubuntu 机器上完美运行;但当我尝试部署应用程序时,catdoc在 Lambda 环境中不存在,这对我来说是一个巨大的问题。

我花了数天数夜来解决这个问题,因为我处于要么按照要求实现它,要么放弃使用 Zappa 进行无服务器实现的情况。放弃 Zappa 对我来说是不可能的,因为我已经爱上了 Zappa,并且使用它开发了许多项目。

幸运的是,我并不是在 Zappa 世界中孤独的。我与 Zappa 社区保持联系,并与 João Neves 先生——一个真正的绅士——会面,他帮助我解决了问题,最终我以非常高效的方式解决了问题。这对我的组织来说是 Zappa 的一次重大胜利。我希望 Zappa 社区,特别是 João Neves 先生,能得到热烈的掌声。

让我们在下一节中揭示 API 的实际实现。

使用自定义工具依赖进行 API 开发

我们的第一个目标是开发一个支持文件上传的 API。这个 API 只能支持单个文件上传,并且需要对文件扩展名进行验证检查。我们将在一个只有.doc扩展名的 MS Office 文档文件上执行操作。因此,这个 API 只允许.doc扩展名的文件。

先决条件

在本章的技术要求部分提到,我们需要使用 Python 版本 3.6 来配置pipenv。我们使用以下命令来初始化带有 Python 3.6 的pipenv环境:

$ pipenv --python python3.6

现在,使用pipenv install命令安装以下软件包:

  • falcon

  • flacon-multipart

  • gunicorn

  • zappa

安装了这些软件包后,pipenv将创建一个Pipfile,如下所示:

[[source]]

url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[dev-packages]

[packages]

falcon = "*"
falcon-multipart = "*"
gunicorn = "*"
zappa = "*"

[requires]

python_version = "3.6"

就是这样,我们的安装工作完成了!现在我们可以使用pipenv shell命令进入虚拟环境的 shell,或者使用pipenv run命令在虚拟环境中运行任何命令。让我们继续实现 API。

实现/doc-parser API

/doc-parser API:
import falcon
from falcon_multipart.middleware import MultipartMiddleware
from parser import doc_to_text

class DocParserResource:
    def on_post(self, req, resp):
        """Handles POST requests"""
        try:
            file_object = req.get_param('file')

            # file validation
            if file_object.type != 'application/msword' or file_object.filename.split('.')[-1] != 'doc':
                raise ValueError('Please provide a valid MS Office 93 -2003 document file.')

            # calling _doc_to_text method from parser.py
            text = doc_to_text(file_object)
            quote = {
                'text': text,
                'file': file_object.filename
            }
            resp.media = quote
        except Exception as e:
            raise falcon.HTTPError(falcon.HTTP_500, str(e))

api = falcon.API(middleware=[MultipartMiddleware()])
api.add_route('/doc-parser', DocParserResource())

在这里,我们创建了一个只有 HTTP POST请求的 API 资源。这个 API 将接受一个文件属性作为多部分数据。一旦文件上传完成,我们将验证文件类型和扩展名。如果文件是application/msword,并且文件扩展名是".doc",那么我们可以继续;否则,我们将返回一个错误。

如果上传的文件有效,我们将继续从文件中解析文本数据,并以 JSON 格式返回数据。为了解析文件,我们在parser.py中编写了doc_to_text方法。

parser.py:
import os

def doc_to_text(file_object):
    data = ''
    # save file in tmp
    filepath = os.path.join('/tmp', file_object.filename)
    with open(filepath, 'wb') as tmp_file:
        while True:
            chunk = file_object.file.read(4096)
            tmp_file.write(chunk)
            if not chunk:
                break

    # Parse and return text data
    with os.popen('catdoc -a {0}'.format(filepath), 'r') as proc:
        data = proc.read()
    return data

正如你在这里看到的,我们正在执行两个不同的任务。首先,我们将上传的文件存储在/tmp 目录中,其次,我们通过使用os.popen命令来运行catdoc命令来解析文本。借助os.popen命令,我们读取catdoc命令的stdoutcatdoc命令有许多选项可用。我正在使用catdoc -a <doc-file>仅读取 ASCII 字符。您可以使用man catdoc命令来探索catdoc

让我们继续下一节,我们将执行此 API。

在本地环境中执行

API 执行需要一个文件上传过程。所以,我建议使用 REST API 客户端。在我们的案例中,我正在使用高级 REST 客户端。以下截图是 API 上传过程:

就是这样。我们的 API 按预期工作。现在我们有了 JSON 格式的文本数据,以及文件名。现在让我们继续下一节,我们将探讨在 AWS Lambda 上使用 Zappa 进行构建过程。我们还将探讨依赖工具如何引发异常,以及 Docker 机制如何帮助解决问题。

使用 Docker 构建,测试和部署

zappa_settings.json file:
{
    "dev": {
        "app_function": "resources.api",
        "aws_region": "ap-south-1",
        "profile_name": "default",
        "project_name": "chapter12",
        "runtime": "python3.6",
        "s3_bucket": "zappa-xl0doooe4"
    }
}

让我们继续进行 Zappa 构建,而不考虑 Docker 环境。

无 Docker 构建

zappa deploy <stage_name> command:
$ zappa deploy dev
Calling deploy for stage dev..
Creating chapter12-dev-ZappaLambdaExecutionRole IAM Role..
Creating zappa-permissions policy on chapter12-dev-ZappaLambdaExecutionRole IAM Role.
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter12-dev-1531957045.zip (5.7MiB)..
100%|██████████████████████████████████████████████████████████████████████████████████████████████| 5.93M/5.93M [00:12<00:00, 372KB/s]
Scheduling..
Scheduled chapter12-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Uploading chapter12-dev-template-1531957066.json (1.6KiB)..
100%|█████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 3.40KB/s]
Waiting for stack chapter12-dev to create (this can take a bit)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:09<00:00, 2.66s/res]
Deploying API Gateway..
Deployment complete!: [`rbupm44rza.execute-api.ap-south-1.amazonaws.com/dev`](https://rbupm44rza.execute-api.ap-south-1.amazonaws.com/dev)

**现在我们已经部署了应用程序,让我们测试 API 执行。以下是使用高级 REST 客户端执行 API 的截图:

在这里,我们尝试上传相同的文件,但没有得到内容。甚至返回响应是 OK。让我们找出原因。这个问题的原因是什么?你可以使用zappa logs <stage_name>来查看 Zappa 日志。

zappa logs command:
[1532008716236] /bin/sh: catdoc: command not found
[1532008716237] [INFO] 2018-07-19T13:58:36.237Z d4b00497-8b5b-11e8-8381-9510b412860f 103.19.39.2 - - [19/Jul/2018:13:58:36 +0000] "POST /doc-parser HTTP/1.1" 200 47 "" "" 0/68.15899999999999

在这里你可以看到我们得到了一个错误,指出catdoc命令找不到。这是真的,也是预期的,因为catdoc在 AWS Lambda 环境中不可用,并且没有办法在 AWS Lambda 上安装这个依赖项。但为什么我们没有得到异常?嗯,这是一个系统级错误,因为我们使用os.popen方法来执行catdoc命令。因此,Python 不会捕获这些错误。这就是我们没有得到异常的原因。

那么问题呢?我们是无助的,因为我们在 AWS Lambda 上无能为力,也无法改变 AWS Lambda 环境!

等等!有人给你带来了一个解决方案—LambCI。LambCI 发布了一个 Docker 镜像(github.com/lambci/docker-lambda),它是 AWS Lambda 环境的镜像。现在你可以用它来解决问题。让我们继续下一节,我们将配置 Docker 环境以及所需的依赖项。

使用 Zappa 配置 Docker

借助 LambCI Docker 镜像,我们将获得与 AWS Lambda 兼容的环境。但是,我们仍然需要与catdoc实用程序相关的构建依赖项。现在,我们可以使用带有build-python3.6标签的 Docker 镜像来创建一个 Docker 容器。

以下是一个显示创建 Docker 容器的代码片段:

$ sudo docker run --name doc-parser -v "$PWD":/var/task -v ~/.aws/:/root/.aws/ -e AWS_PROFILE=default -p "8000:8000" -it lambci/lambda:build-python3.6 bash
docker run command along with some options. These options are used to configure the container and set up the container environment. Let's have a look at these options:
  • run:此命令用于基于给定的图像标签创建和启动容器。在我们的案例中,我们使用"lambci/lambda:build-python3.6"

  • --name:此选项用于创建 Docker 容器的名称。

  • -v:此选项用于将目录从主机机器挂载到 Docker 容器。对于多个目录,我们需要重复此选项,因为我们正在挂载当前目录以及 AWS CLI 目录以获取 AWS 访问凭据。

  • -e:此选项用于将环境变量设置到 Docker 容器中。对于多个环境变量,您需要重复此选项。

  • -p:此选项用于将 Docker 容器端口暴露和映射到主机。我们映射到端口8000,这样我们可以在本地环境中测试应用程序。

  • **-it**:此选项用于以交互模式启动 Docker 容器,我们可以与 Docker 容器进行交互。

还有要执行的bash命令。此命令将使我们进入 Docker 容器的终端 Bash 屏幕。运行该命令后,它将启动 Docker 容器并将控制器附加到 Bash 屏幕。

看一下刚才提到的选项的日志片段,并检查挂载的文件:

$ sudo docker run --name doc-parser -v "$PWD":/var/task -v ~/.aws/:/root/.aws/ -e AWS_PROFILE=default -p "8000:8000" -it lambci/lambda:build-python3.6 bash
bash-4.2# ls
parser.py Pipfile Pipfile.lock __pycache__ resources.py sample-doc-file.doc zappa_settings.json
bash-4.2# pwd
/var/task

如您所见,我们当前目录中的所有文件都已根据卷映射挂载。现在,该容器的上下文与 AWS Lambda 的上下文类似。因此,我们可以配置任何库或工具的任何源代码。

我们现在将看一下catdoc的配置。如 catdoc 网站(www.wagner.pp.ru/~vitus/software/catdoc/)所述,您可以通过系统下载源代码并进行编译。我们将使用wget工具在容器内下载catdoc源代码。

在那之前,我们需要在容器中安装wget工具,如下代码片段所示:

bash-4.2# yum install wget
Resolving Dependencies
--> Running transaction check
---> Package wget.x86_64 0:1.18-3.27.amzn1 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

=========================================================================================================================================================================
 Package Arch Version Repository Size
=========================================================================================================================================================================
Installing:
 wget x86_64 1.18-3.27.amzn1 amzn-updates 981 k

Transaction Summary
=========================================================================================================================================================================
Install 1 Package

Total download size: 981 k
Installed size: 2.4 M
Is this ok [y/d/N]: y
Downloading packages:
wget-1.18-3.27.amzn1.x86_64.rpm | 981 kB 00:00:01 
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : wget-1.18-3.27.amzn1.x86_64 1/1 
  Verifying : wget-1.18-3.27.amzn1.x86_64 1/1 

Installed:
  wget.x86_64 0:1.18-3.27.amzn1 

Complete!

安装了wget工具后,下载catdoc源代码到一个文件夹中。在我们的情况下,我们将其下载到lib文件夹中。您可以按照以下方式创建libusr目录:

bash-4.2# mkdir lib
bash-4.2# mkdir usr
bash-4.2# ls
***lib*** parser.py Pipfile Pipfile.lock __pycache__ resources.py sample-doc-file.doc ***usr*** zappa_settings.json
bash-4.2# 

libusr目录用于任何库的编译源代码,因此这些目录需要维护编译源代码的二进制文件以供执行。

现在是时候从源代码安装catdoc库了。您需要按照以下步骤配置库:

  1. 下载catdoc源代码,如下代码所示:
bash-4.2# wget http://ftp.wagner.pp.ru/pub/catdoc/catdoc-0.95.tar.gz -O lib/catdoc-0.95.tar.gz
--2018-07-19 23:00:39-- http://ftp.wagner.pp.ru/pub/catdoc/catdoc-0.95.tar.gz
Resolving ftp.wagner.pp.ru (ftp.wagner.pp.ru)... 78.46.190.96, 2a01:4f8:c17:2e5b::2
Connecting to ftp.wagner.pp.ru (ftp.wagner.pp.ru)|78.46.190.96|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 217779 (213K) [application/x-gzip]
Saving to: ‘lib/catdoc-0.95.tar.gz’

lib/catdoc-0.95.tar.gz 100%[=====================================================================================>] 212.67K --.-KB/s in 0.07s 

2018-07-19 23:00:40 (2.93 MB/s) - ‘lib/catdoc-0.95.tar.gz’ saved [217779/217779]

bash-4.2# 
  1. 现在使用tar命令行实用程序解压缩文件:
bash-4.2# cd lib/
bash-4.2# ls
catdoc-0.95.tar.gz
bash-4.2# tar -xf catdoc-0.95.tar.gz 
bash-4.2# ls
catdoc-0.95 catdoc-0.95.tar.gz
  1. 接下来,进入catdoc源目录,并配置它的前缀,以保持二进制文件在应用程序级别,如下代码片段所示:
bash-4.2# ls
catdoc-0.95 catdoc-0.95.tar.gz
bash-4.2# cd catdoc-0.95
bash-4.2# ls
acconfig.h CODING.STD configure COPYING INSTALL install-sh missing NEWS src
charsets compat configure.in doc INSTALL.dos Makefile.in mkinstalldirs README TODO
bash-4.2# ./configure --prefix=/var/task/usr/
  1. 现在运行makemake install命令,如下所示:
bash-4.2# make
...
...
...
bash-4.2# make install
  1. 现在您会发现catdoc二进制文件可在/var/task/usr/目录中找到,如下代码所示:
bash-4.2# ls /var/task/usr/bin/
catdoc catppt wordview xls2csv
bash-4.2# cd /var/task/
bash-4.2# ls
lib parser.py Pipfile Pipfile.lock __pycache__ resources.py sample-doc-file.doc usr zappa_settings.json
  1. 更改parser.py中的以下行,我们只是更改了命令路径:
import os

def doc_to_text(file_object):
    data = ''
    # save file in tmp
    filepath = os.path.join('/tmp', file_object.filename)
    with open(filepath, 'wb') as tmp_file:
        while True:
            chunk = file_object.file.read(4096)
            tmp_file.write(chunk)
            if not chunk:
                break

    # Parse and return text data
    with os.popen('./usr/bin/catdoc -a {0}'.format(filepath), 'r') as proc:
        data = proc.read()
    return data

就是这样!我们现在已经在我们的应用程序中添加了catdoc依赖项。这个依赖项已经在 Docker 容器中配置,而我们的 Docker 容器和 AWS Lambda 环境具有相同的操作系统上下文。因此,配置的依赖项也将在 AWS Lambda 上运行。

让我们继续从容器本身部署应用程序。在初始化部署之前,我们需要使用pipenv安装所有必需的 Python 包:

  1. 以下日志片段显示了pipenv install命令:
bash-4.2# pipenv install
Creating a virtualenv for this project...
Pipfile: /var/task/Pipfile
Using /var/lang/bin/python3.6m (3.6.1) to create virtualenv...
Running virtualenv with interpreter /var/lang/bin/python3.6m
Using base prefix '/var/lang'
New python executable in /root/.local/share/virtualenvs/task-rlWbeMzF/bin/python3.6m
Also creating executable in /root/.local/share/virtualenvs/task-rlWbeMzF/bin/python
Installing setuptools, pip, wheel...done.
Setting project for task-rlWbeMzF to /var/task

Virtualenv location: /root/.local/share/virtualenvs/task-rlWbeMzF
Installing dependencies from Pipfile.lock (5f0d9b)...
Ignoring futures: markers 'python_version < "3"' don't match your environment
Looking in indexes: https://pypi.python.org/simple
   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 37/37 — 00:00:45
To activate this project's virtualenv, run pipenv shell.
Alternativaly, run a command inside the virtualenv with pipenv run.
bash-4.2# 
  1. 现在,使用pipenv shell命令激活虚拟环境,如下所示:
bash-4.2# pipenv shell
Please ensure that the SHELL environment variable is set before activating shell.
bash-4.2# 

哎呀!在激活虚拟环境时出现了错误!让我们修复它,然后再次激活虚拟环境:

bash-4.2# export SHELL=/bin/bash pipenv shell
bash-4.2# pipenv shell
Spawning environment shell (/bin/bash). Use 'exit' to leave.
 . /root/.local/share/virtualenvs/task-rlWbeMzF/bin/activate
bash-4.2# . /root/.local/share/virtualenvs/task-rlWbeMzF/bin/activate
(task-rlWbeMzF) bash-4.2# 

我们设置了SHELL环境变量,然后重新运行 Zappa shell命令。因此,虚拟环境已被激活。现在,让我们继续。

Zappa 需要启用虚拟环境,因为它基于虚拟环境中安装的包构建部署包。

  1. 使用zappa deploy命令或zappa update命令执行部署。我们已经部署了应用程序,所以我们将使用zappa update
(task-rlWbeMzF) bash-4.2# zappa update dev
(python-dateutil 2.7.3 (/var/runtime), Requirement.parse('python-dateutil<2.7.0,>=2.6.1'), {'zappa'})
Calling update for stage dev..
Downloading and installing dependencies..
 - sqlite==python36: Using precompiled lambda package
Packaging project as zip.
Uploading chapter12-dev-1532044423.zip (6.5MiB)..
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 6.78M/6.78M [00:14<00:00, 286KB/s]
Updating Lambda function code..
Updating Lambda function configuration..
Uploading chapter12-dev-template-1532044458.json (1.6KiB)..
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.62K/1.62K [00:00<00:00, 5.20KB/s]
Deploying API Gateway..
Scheduling..
Unscheduled chapter12-dev-zappa-keep-warm-handler.keep_warm_callback.
Scheduled chapter12-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Your updated Zappa deployment is live!: https://rbupm44rza.execute-api.ap-south-1.amazonaws.com/dev
  1. 现在我们已经完成了部署。让我们继续前进到下一部分并探索 API 的执行。

在 AWS Lambda 上执行 API

您可以使用任何 REST 客户端来调试和执行 API。我们将使用高级 REST 客户端。以下截图演示了 API 的执行:

正如您在这里所看到的,我们已上传了 MS Office 文档文件,并以 JSON 格式收到了包含上传文件中所有文本数据的响应。任务完成。

总结

最后,我们已经实现了使用自定义依赖项开发应用程序的解决方案。借助 Docker 容器化,我们已经构建了所需的catdoc库的二进制文件,该库针对 Docker 容器进行了配置,结果与我们预期的 AWS Lambda 类似,这要归功于 LambCI 的 Docker 镜像和build-Python3.6标签。这就是我们如何通过 AWS Lambda 解决自定义依赖问题的方法。

问题

  1. Docker 容器是如何工作的?

  2. Docker 镜像和 Docker 容器之间有什么区别?

第十三章:评估

第一章,无服务器的亚马逊网络服务

  1. 在无服务器架构上部署应用程序只是将应用程序交给亚马逊基础设施。因此,有以下好处:
  • 亚马逊将负责自动扩展

  • 不需要服务器管理流程

  • 在成本方面也有很大的差异,您按照基于执行时间的使用量付费。

  • 它提供高可用性

  1. Amazon Simple Storage Service (S3)是亚马逊提供的存储服务。AWS Lambda 支持内联代码执行,您可以直接从其 Web 界面编写代码。它还支持从 Amazon S3 存储桶中获取代码库,您可以将代码库放入 ZIP 格式的构建包中。Zappa 有一个命令来生成应用程序的 ZIP 包。

第二章,开始使用 Zappa

  1. 这是由 gun.io (www.gun.io/)开发的开源工具,用于自动化在 AWS 基础设施上创建无服务器环境的手动过程。

  2. Zappa 通过在zappa_setttings.json中添加 AWS VPC 子网和安全组 ID 来提供配置 AWS VPC (虚拟私有云)的简单方法。

第三章,使用 Zappa 构建 Flask 应用

  1. 亚马逊 API Gateway 是一个连接其他 AWS 服务的服务。API Gateway 为移动和 Web 应用程序提供了与其他 AWS 服务连接的 RESTful 应用程序接口。在我们的案例中,Zappa 配置了 API Gateway 接口,以代理请求来调用 AWS Lambda。

  2. Zappa 根据zappa_settings.json文件的配置执行部署操作。Zappa 使用function_name指向 Flask 应用对象,以便在 AWS Lambda 和 API Gateway 上配置应用程序。

第四章,使用 Zappa 构建基于 Flask 的 REST API

  1. JWT (JSON Web Token)提供了一种简单的方式来保护应用程序免受未经授权的访问。根据身份验证标头中提供的 JWT 令牌,可以授权对 API 的访问。

  2. function_name指示了 Flask 应用对象的模块路径。它帮助 Zappa 配置 Flask 应用程序及其路由与 API Gateway。

第五章,使用 Zappa 构建 Django 应用

  1. Amazon CloudFront 是一个托管的网络服务,可以通过互联网高速传送静态和动态网络内容。亚马逊在全球各地有各种数据中心,这些数据中心被称为边缘位置,因此 AWS CloudFront 使用这些边缘位置以最小的延迟传送网络内容,并提高应用程序的性能。

  2. Pipenv 是一个用于管理 Python 包的打包工具。它也被Python.org (www.python.org/)推荐。它有助于维护包和依赖项以及它们的版本。因此,它有助于开发和维护稳定版本的应用程序。

第六章,使用 Zappa 构建 Django REST API

  1. Django Rest Framework 是一个用于开发基于 Django 应用的 RESTful API 的库。它有一个标准模式来在 Django 模型上实现 API。它为开发人员提供了许多功能,以简单的方式实现和管理 API。

  2. Django-storage 是一个用于实现 Django 应用程序的自定义存储的库。它遵循 Django 的标准以持久化数据。

第七章,使用 Zappa 构建 Falcon 应用

  1. 与其他 Python 框架相比,Falcon 框架具有很高的基准。它旨在以非常优化的方式编写 RESTful API。

  2. Peewee 库遵循 Django 的模式来创建数据库表并执行 CRUD 操作。它提供许多功能,如高性能、轻量级和较少的复杂性。SQLAlchemy 有一点学习曲线和复杂性。Peewee 可以被认为是一个小型/中型的应用程序或微服务。

  3. 调度是在特定时间段执行程序的定义机制。因此,它与许多场景一起使用,其中我们需要执行程序或脚本以执行特定时间。例如,更新天气信息,发送通知警报等。

第八章,带 SSL 的自定义域

  1. AWS Route 53 是亚马逊的托管服务。它提供域名注册服务,将互联网流量路由到特定域的 AWS 资源,并为正在运行的 AWS 资源创建健康检查点。

  2. 域名服务器DNS)是一种维护和转换域名到Internet ProtocolIP)的机制,因为计算机通过 IP 地址进行通信并且很难记住。因此 DNS 有助于管理域名与 IP 地址的对应关系。

  3. ACM 根据域名生成 SSL 证书。如果您在域名上使用 SSL 证书,它将启用 HTTPS 协议,用于通过您的域进行过渡。HTTPS 是一种安全协议,它加密了互联网上传输的数据,并为通过您的域传输的机密信息提供了安全性。

第九章,在 AWS Lambda 上执行异步任务

  1. AWS SNS 是一个 Web 服务,提供发布和订阅模式的消息实现。它支持各种资源订阅通道并获取发布的消息。它可以用于管理和实现应用程序的通知服务。还有许多其他功能,可以考虑将 AWS SNS 服务用于应用程序开发。

  2. AWS SNS 用于发布和订阅模式。它支持将 AWS Lambda 注册为订阅者。它可以使用发布的消息上下文调用 AWS Lambda 函数。

第十章,高级 Zappa 设置

  1. AWS Lambda 旨在提供无服务器基础架构。它在调用请求时实例化上下文,然后在提供请求后销毁自身。AWS Lambda 会为初始启动和上下文设置添加一点时间延迟。为了避免这种情况,您可以通过使用 AWS CloudWatch 设置计划触发器来保持 Lambda 实例处于热状态。Zappa 默认提供了这个功能,您可以通过将keep_warm属性设置为false来关闭此功能。

  2. 跨域资源共享CORS)是一种机制,允许一个域从不同的域访问受限资源。

  3. Zappa 提供了一种管理大型项目的简单方法,因为 AWS Lambda 在上传构建包时有 50MB 的限制,但也有一个选项可以从 Amazon S3 服务更大的构建包。在 Zappa 设置中,您可以将slim_handler设置为true,这将在 Amazon S3 上上传构建包,然后针对在 Amazon S3 上上传的构建包配置 AWS Lambda。

第十一章,使用 Zappa 保护无服务器应用程序

  1. API Gateway 授权者是一种保护 API 资源的机制。API Gateway 授权者生成一个 API 密钥,可以绑定到任何资源。一旦绑定了 API 资源,API Gateway 将限制任何带有x-api-key头的 HTTP 请求。

  2. AWS Lambda 具有死信队列DLQ)的功能,它使开发人员能够监视未知的失败。它可以配置为 AWS Lambda 函数中的 AWS SNS 或 SQS 的 DLQ。AWS Lambda 将在配置的 AWS SNS 或 SQS ARN 上发布失败事件。

  3. AWS 虚拟私有云创建了一个隔离的虚拟网络层,您可以在其中配置所有 AWS 资源。AWS VPC 将限制从 VPC 网络外部的访问,并启用安全层。

第十二章,Zappa 与 Docker

  1. Docker 容器是基本 Linux 系统的虚拟实例,它使您能够在隔离的环境中执行操作。Docker 容器具有所有基本配置,如网络、文件系统和操作系统级实用程序。

  2. 一个 Docker 镜像是一个带有所需软件包的实际操作系统镜像。您也可以创建自己的镜像并将其发布到 Docker 仓库。一个 Docker 容器是 Docker 镜像的一个实例。您可以使用 Docker 镜像创建N个容器。

posted @ 2024-05-20 16:53  绝不原创的飞龙  阅读(11)  评论(0编辑  收藏  举报