Go-无服务应用实用指南(全)

Go 无服务应用实用指南(全)

原文:zh.annas-archive.org/md5/862FBE1FF9A9C074341990A4C2200D42

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

无服务器架构在技术社区中很受欢迎,其中 AWS Lambda 是很受欢迎的。Go 语言易于学习,易于使用,并且易于其他开发人员阅读,现在它已被誉为 AWS Lambda 支持的语言。本书是您设计无服务器 Go 应用程序并将其部署到 Lambda 的最佳指南。

本书从快速介绍无服务器架构及其优势开始,然后通过实际示例深入探讨 AWS Lambda。然后,您将学习如何在 Go 中使用 AWS 无服务器服务设计和构建一个可投入生产的应用程序,而无需预先投资基础设施。本书将帮助您学习如何扩展无服务器应用程序并在生产中处理分布式无服务器系统。然后,您还将学习如何记录和测试您的应用程序。

在学习的过程中,您还将发现如何设置 CI/CD 管道以自动化 Lambda 函数的部署过程。此外,您将学习如何使用 AWS CloudWatch 和 X-Ray 等服务实时监视和排除故障您的应用程序。本书还将教您如何扩展无服务器应用程序并使用 AWS Cognito 安全访问。

通过本书,您将掌握设计、构建和部署基于 Go 的 Lambda 应用程序到生产的技能。

这本书适合谁

这本书适合希望了解无服务器架构的 Gophers。假定具有 Go 编程知识。对于有兴趣在 Go 中构建无服务器应用程序的 DevOps 和解决方案架构师也会从本书中受益。

本书涵盖了什么

第一章《Go 无服务器》给出了关于无服务器是什么,它是如何工作的,它的特点是什么,为什么 AWS Lambda 是无服务器计算服务的先驱,以及为什么您应该使用 Go 构建无服务器应用程序的基础解释。

第二章《开始使用 AWS Lambda》提供了在 Go 运行时和开发环境旁边设置 AWS 环境的指南。

第三章《使用 Lambda 开发无服务器函数》描述了如何从头开始编写您的第一个基于 Go 的 Lambda 函数,以及如何从控制台手动调用它。

第四章《使用 API Gateway 设置 API 端点》说明了如何使用 API Gateway 在收到 HTTP 请求时触发 Lambda 函数,并构建一个由无服务器函数支持的统一事件驱动的 RESTful API。

第五章《使用 DynamoDB 管理数据持久性》展示了如何通过使用 DynamoDB 数据存储解决 Lambda 函数无状态问题来管理数据。

第六章《部署您的无服务器应用程序》介绍了在构建 AWS Lambda 中的无服务器函数时可以使用的高级 AWS CLI 命令和选项,以节省时间。它还展示了如何创建和维护 Lambda 函数的多个版本和发布。

第七章《实施 CI/CD 管道》展示了如何设置持续集成和持续部署管道,以自动化 Lambda 函数的部署过程。

第八章《扩展您的应用程序》介绍了自动缩放的工作原理,Lambda 如何在高峰服务使用期间处理流量需求而无需容量规划或定期缩放,以及如何使用并发预留来限制执行次数。

第九章《使用 S3 构建前端》说明了如何使用由无服务器函数支持的 REST 后端构建单页面应用程序。

第十章,测试您的无服务器应用程序,展示了如何使用 AWS 无服务器应用程序模型在本地测试无服务器应用程序。它还涵盖了 Go 单元测试和使用第三方工具进行性能测试,并展示了如何使用 Lambda 执行测试工具。

第十一章,监控和故障排除,进一步介绍了如何使用 CloudWatch 设置函数级别的监控,以及如何使用 AWS X-Ray 调试和故障排除 Lambda 函数,以便对应用程序进行异常行为检测。

第十二章,保护您的无服务器应用程序,致力于在 AWS Lambda 中遵循最佳实践和建议,使您的应用程序符合 AWS Well-Architected Framework 的要求,从而使其具有弹性和安全性。

第十三章,设计成本效应应用程序,还介绍了一些优化和减少无服务器应用程序计费的技巧,以及如何使用实时警报跟踪 Lambda 的成本和使用情况,以避免问题的出现。

第十四章,基础设施即代码,介绍了诸如 Terraform 和 SAM 之类的工具,帮助您以自动化方式设计和部署 N-Tier 无服务器应用程序,以避免人为错误和可重复的任务。

要充分利用本书

本书适用于在 Linux、Mac OS X 或 Windows 下工作的任何人。您需要安装 Go 并拥有 AWS 账户。您还需要 Git 来克隆本书提供的源代码库。同样,您需要具备 Go、Bash 命令行和一些 Web 编程技能的基础知识。所有先决条件都在第二章中进行了描述,开始使用 AWS Lambda,并提供了确保您能轻松跟随本书的说明。

最后,请记住,本书并不意味着取代在线资源,而是旨在补充它们。因此,您显然需要互联网访问来完成阅读体验的某些部分,通过提供的链接。

下载示例代码文件

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

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

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

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

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

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

下载文件后,请确保使用最新版本的解压缩软件解压缩文件夹:

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

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

我们还有来自丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

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

使用的约定

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些样式的示例及其含义的解释。

文本中的代码单词显示如下:“在工作空间中,使用vim创建一个main.go文件,内容如下。”

代码块设置如下:

package main
import "fmt"

func main(){
  fmt.Println("Welcome to 'Hands-On serverless Applications with Go'")
}

任何命令行输入或输出都是这样写的:

pip install awscli

粗体:表示一个新术语,一个重要词,或者您在屏幕上看到的词。例如,菜单或对话框中的单词会在文本中显示为这样。这里有一个例子:“在Source页面上,选择GitHub作为源提供者。”

警告或重要说明看起来像这样。

提示和技巧看起来像这样。

第一章:Go 无服务器

本章将为您提供对无服务器架构是什么,它是如何工作的,以及它的特点是什么的基础理解。您将了解到AWS Lambda如何与谷歌云函数和微软 Azure 函数等大型参与者不相上下。然后,您将了解 AWS Lambda 的不同执行环境及其对 Go 语言的支持。此外,我们将讨论使用 Go 作为构建无服务器应用程序的编程语言的优势。

本章将涵盖以下主题:

  • 云计算模型——了解它们是什么以及可以用于什么。

  • 无服务器架构的优缺点。

  • 为什么 Go 非常适合 AWS Lambda。

无服务器范式

基于云的应用程序可以构建在低级基础设施部件上,也可以使用提供抽象层的更高级服务,从而摆脱核心基础设施的管理、架构和扩展要求。在接下来的部分,您将了解不同的云计算模型。

云计算的演进

云提供商根据四种主要模型提供其服务:IaaS、PaaS、CaaS 和 FaaS。所有上述模型只是在底层添加了数千台服务器、磁盘、路由器和电缆。它们只是在顶部添加了抽象层,以使管理更容易,并增加开发速度。

基础设施即服务

基础设施即服务IaaS),有时缩写为 IaaS,是基本的云消费模型。它在虚拟化平台之上构建了一个 API,以访问计算、存储和网络资源。它允许客户无限扩展其应用程序(无需容量规划)。

在这种模型中,云提供商抽象了硬件和物理服务器,云用户负责管理和维护其上的客户操作系统和应用程序。

根据 Gartner 的基础设施即服务魔力象限图,AWS 是领先者。无论您是在寻找内容传递、计算能力、存储还是其他服务功能,AWS 在 IaaS 云计算模型方面是各种可用选项中最有利的。它主导着公共云市场,而微软 Azure 正在逐渐赶上亚马逊,其次是谷歌云平台和 IBM 云。

平台即服务

平台即服务PaaS)为开发人员提供了一个框架,他们可以在其中开发应用程序。它简化、加快了开发、测试和部署应用程序的过程,同时隐藏了所有实现细节,如服务器管理、负载均衡器和数据库配置。

PaaS 建立在 IaaS 之上,因此隐藏了底层基础设施和操作系统,使开发人员能够专注于提供业务价值并减少运营开销。

Heroku 是最早推出 PaaS 的之一,于 2007 年;后来,谷歌应用引擎和 AWS 弹性 Beanstalk 也加入了竞争。

容器即服务

容器即服务CaaS)随着 2013 年 Docker 的发布而变得流行。它使得在本地数据中心或云上构建和部署容器化应用变得容易。

容器改变了 DevOps 和站点可靠性工程师的规模单位。多个容器可以在单个虚拟机上运行,这样可以更好地利用服务器并降低成本。它还通过消除“在我的机器上运行”的笑话,使开发人员和运维团队更加紧密地联系在一起。这种转变到容器使多家公司能够现代化其传统应用程序并将其迁移到云上。

为了实现容错、高可用性和可伸缩性,需要一个编排工具,比如 Docker Swarm、Kubernetes 或 Apache Mesos,来管理节点集群中的容器。因此,引入了 CaaS 来快速高效地构建、部署和运行容器。它还处理了诸如集群管理、扩展、蓝/绿部署、金丝雀更新和回滚等重型任务。

市场上最流行的 CaaS 平台是 AWS,因为 57%的 Kubernetes 工作负载运行在亚马逊弹性容器服务ECS)、弹性 Kubernetes 服务EKS)和 AWS Fargate 上,其次是 Docker Cloud、CloudFoundry 和 Google 容器引擎。

这种模型,CaaS,使您能够进一步分割虚拟机以实现更高的利用率,并在机器集群中编排容器,但云用户仍然需要管理容器的生命周期;作为解决方案,引入了函数即服务FaaS)。

函数即服务

FaaS 模型允许开发人员在不需要预配或维护复杂基础设施的情况下运行代码(称为函数)。云提供商将客户代码部署到完全托管的、临时的、有时间限制的容器中,这些容器仅在函数调用期间处于活动状态。因此,企业可以在不必担心扩展或维护复杂基础设施的情况下实现增长;这被称为无服务器化。

亚马逊在 2014 年推出了 AWS Lambda,开启了无服务器革命,随后是微软 Azure Functions 和 Google Cloud Functions。

无服务器架构

无服务器计算,或者说 FaaS,是云计算的第四种消费方式。在这种模式下,预配、维护和打补丁的责任从客户转移到了云提供商。开发人员现在可以专注于构建新功能和创新,并且只支付他们消耗的计算时间。

无服务器化的好处

无服务器化有很多合理之处:

  • 无运维:服务器基础设施由云提供商管理,这减少了开销并提高了开发速度。操作系统更新和补丁由 FaaS 提供商处理。这导致了缩短的上市时间和更快的软件发布,消除了系统管理员的需求。

  • 自动扩展和高可用性:作为规模的单位,函数导致了小型、松耦合和无状态的组件,从长远来看,这会导致可伸缩的应用程序。如何有效地利用基础设施来为客户提供服务请求并根据负载水平扩展函数,这取决于服务提供商。

  • 成本优化:您只支付您消耗的计算时间和资源(RAM、CPU、网络或调用时间)。您不支付闲置资源。没有工作意味着没有成本。例如,如果 Lambda 函数的计费周期为 100 毫秒,那么它可以显著降低成本。

  • 多语言:无服务器方法带来的一个好处是,作为程序员,您可以根据您的用例选择不同的语言运行时。应用程序的一部分可以用 Java 编写,另一部分可以用 Go 编写,另一部分可以用 Python 编写;只要能完成工作,就没有关系。

无服务器化的缺点

另一方面,无服务器计算仍处于起步阶段;因此,并不适用于所有用例,并且它确实有其局限性:

  • 透明性:基础设施由 FaaS 提供商管理。这是为了灵活性;您无法完全控制您的应用程序,无法访问底层基础设施,也无法在不同平台提供商之间切换(供应商锁定)。未来,我们预计将会有更多工作朝着 FaaS 的统一化方向发展;这将有助于避免供应商锁定,并允许我们在不同的云提供商甚至本地运行无服务器应用程序。

  • 调试:监控和调试工具并非是针对无服务器架构而构建的。因此,无服务器函数很难进行调试和监控。此外,在部署之前很难设置本地环境来测试您的函数(预集成测试)。好消息是,随着无服务器的普及和社区和云提供商创建了多个开源项目和框架(如 AWS X-Ray、Datadog、Dashbird 和 Komiser),最终会出现工具来改善无服务器环境的可观察性。

  • 冷启动:处理函数的第一个请求需要一些时间,因为云提供商需要为您的任务分配适当的资源(AWS Lambda 需要启动一个容器)。为了避免这种情况,您的函数必须保持活动状态。

  • 无状态:函数需要是无状态的,以提供使无服务器应用程序能够透明扩展的提供。因此,要持久保存数据或管理会话,您需要使用外部数据库,如 DynamoDB 或 RDS,或内存缓存引擎,如 Redis 或 Memcached。

尽管已经说明了所有这些限制,但这些方面将随着越来越多的供应商推出升级版本的平台而发生变化。

无服务器云提供商

有多个 FaaS 提供商,但为了简单起见,我们只比较最大的三个:

  • AWS Lambda

  • Google Cloud Functions

  • Microsoft Azure Functions

以下是一张图示比较:

如前图所示,AWS Lambda 是当今无服务器空间中使用最广泛、最知名和最成熟的解决方案,这就是为什么即将到来的章节将完全专注于 AWS Lambda。

AWS Lambda

AWS Lambda 是 AWS 无服务器平台的核心:

AWS Lambda 在 2014 年的 re:Invent 上推出。这是无服务器计算的第一个实现,用户可以将他们的代码上传到 Lambda。它会代表用户执行操作和管理活动,包括提供容量、监控舰队健康状况、应用安全补丁、部署他们的代码,并将实时日志和指标发布到 Amazon CloudWatch。

Lambda 遵循事件驱动架构。您的代码会在响应事件时触发并并行运行。每个触发器都会被单独处理。此外,您只需按执行次数收费,而使用 EC2 时则按小时计费。因此,您可以以低成本和零前期基础设施投资获得应用程序的自动扩展和容错能力。

事件源

AWS Lambda 根据事件运行您的代码。当这些事件源检测到事件时,将调用您的函数:

Amazon 现在支持 SQS 作为 Lambda 的事件源

使用情况

AWS Lambda 可用于无尽的应用场景:

  • Web 应用程序:您可以使用 S3 和 Lambda 来代替维护带有 Web 服务器的专用实例来托管您的静态网站,以便以更低的成本获得可伸缩性。下图描述了一个无服务器网站的示例:

Route 53中的别名记录指向CloudFront分发。CloudFront分发建立在S3 Bucket之上,其中托管着静态网站。CloudFront减少了对静态资产(JavaScript、CSS、字体和图像)的响应时间,提高了网页加载时间,并减轻了分布式拒绝服务(DDoS)攻击。然后,来自网站的 HTTP 请求通过API Gateway HTTP 端点,触发正确的Lambda Function来处理应用程序逻辑并将数据持久保存到完全托管的数据库服务,如DynamoDB

  • 移动和物联网:构建传感器应用程序的示意图,该应用程序从实时传感器连接的设备中测量温度,并在温度超出范围时发送短信警报,如下所示:

连接设备将数据摄入到AWS IoTAWS IoT规则将调用Lambda 函数以分析数据,并在紧急情况下向SNS 主题发布消息。发布消息后,Amazon SNS 将尝试将该消息传递给订阅主题的每个端点。在这种情况下,它将是短信

  • 数据摄入:监控日志并保持审计跟踪是强制性的,您应该意识到云基础设施中的任何安全漏洞。以下图表说明了一个实时日志处理管道与 Lambda:

VPC 流日志功能捕获有关 VPC 中网络接口的 IP 流量信息,并将日志发送到 Amazon CloudWatch 日志。AWS CloudTrail 记录您帐户上的所有 AWS API 调用。所有日志都被聚合并流式传输到 AWS Kinesis 数据流。

Kinesis 触发 Lambda 函数,分析日志以查找事件或模式,并在异常活动发生时向 Slack 或 PagerDuty 发送通知。最后,Lambda 将数据集发布到预安装了 Kibana 的 Amazon Elasticsearch,以可视化和分析网络流量和日志,使用动态和交互式仪表板。这是为了长期保留和存档日志,特别是对于具有合规性计划的组织。Kinesis 将日志存储在 S3 存储桶中进行备份。可以配置存储桶的生命周期策略,将未使用的日志存档到 Glacier。

  • 定时任务:定时任务和事件非常适合 Lambda。您可以使用 Lambda 创建备份,生成报告和执行 cron 作业,而不是保持实例 24/7 运行。以下示意图描述了如何使用 AWS Lambda 执行后处理作业:

当视频到达 S3 存储桶时,事件将触发一个 Lambda 函数,该函数将视频文件名和路径传递给弹性转码器管道,以执行视频转码,生成多种视频格式(.avi,.h264,.webm,.mp3 等),并将结果存储在 S3 存储桶中。

  • 聊天机器人和语音助手:您可以使用自然语言理解NLU)或自动语音识别ASR)服务,如 Amazon Lex,构建可以触发 Lambda 函数以响应语音命令或文本的应用程序机器人。以下图表描述了使用 Lambda 构建个人助手的用例:

用户可以询问Amazon Echo关于其待办事项清单。Echo 将拦截用户的语音命令并将其传递给自定义Alexa 技能,该技能将进行语音识别并将用户的语音命令转换为意图,触发Lambda 函数,然后查询Trello API 以获取今天的任务列表。

由于 Lambda 在内存、CPU 和超时执行方面的限制,它不适用于长时间运行的工作流和其他大规模工作负载。

Go 无服务器

AWS 在 2018 年 1 月宣布支持 Go 作为 AWS Lambda 的语言。已经有一些开源框架和库可以用来支持使用 Node.js 的 Go 应用程序(Apex 无服务器框架),但现在 Go 已经得到官方支持,并添加到可以用来编写 Lambda 函数的编程语言列表中:

  • Go

  • Node.js

  • Java

  • Python

  • .NET

但是我们应该使用哪种语言来编写高效的 Lambda 函数呢?无服务器的一个原因是多语言。无论您选择哪种语言,编写 Lambda 函数的代码都有一个共同的模式。同时,您需要特别注意性能和冷启动。这就是 Go 发挥作用的地方。以下图表突出了在 AWS Lambda 中使用 Go 进行无服务器应用程序的主要优势:

  • 面向云:它是由谷歌专门为云设计的,考虑到可扩展性,并减少构建时间。Go 是分布式系统和基础设施工具的坚实语言。Docker、Kubernetes、Terraform、etcd、Prometheus 等许多编排、提供和监控工具都是使用 Go 构建的。

  • 快速:Go 编译成单个二进制文件。因此,您可以向 AWS Lambda 提供预编译的 Go 二进制文件。AWS 不会为您编译 Go 源文件,这会产生一些后果,比如快速的冷启动时间。Lambda 不需要设置运行时环境;另一方面,Java 需要启动 JVM 实例来使您的函数热起来。Go 具有清晰的语法和明确的语言规范。这为开发人员提供了一种易于学习的语言,并在产生可维护的代码的同时快速显示出良好的结果。

  • 可扩展:Go 具有内置的 goroutines 并发,而不是线程。它们从堆中消耗了几乎 2 Kb 的内存,并且比线程工作得更快;因此,您可以随时启动数百万个 goroutine。对于软件开发,不需要框架;Golang 社区已经构建了许多工具,这些工具受到 Go 语言核心的本地支持:

  • Go 的错误处理很优雅。

  • 轻量级的单元测试框架。

  • 标准库稳固—HTTP 协议支持开箱即用。

  • 支持的常见数据类型和结构—映射、数组、结构等。

  • 高效:它涉及高效的执行和编译。Go 是一种编译语言;它编译成单个二进制文件。它使用静态链接将所有依赖项和模块组合成一个单个的二进制文件。此外,它更快的编译速度允许快速反馈。快速的开发节省时间和金钱;因此,这对于预算紧张的人来说无疑是最重要的优势。

  • 不断增长的社区:以下截图显示了(根据 StackOverflow Survey 2017 观察到的)最受喜爱、最受恐惧和最想要的编程语言的流行度和使用率:

此外,Go 得到了谷歌的支持,并拥有一个庞大、不断增长的生态系统和众多 GitHub 上的贡献者,以及出色的 IDE 支持(IntelliJ、VSCode、Atom、GoGland)和调试功能。

总结

AWS Lambda 是无服务器计算或 FaaS 的第一个成功实现。它使用户摆脱了管理服务器的束缚,提高了开发速度,降低了系统复杂性,并使小型企业能够在零前期基础设施投资的情况下扩大规模。

对于在 Lambda 上运行业务的人来说,Go 对 AWS Lambda 的支持可以显著节省成本并提高性能。所以如果你正在寻找一种现代、快速、安全、易用的语言,Go 就是你的选择。

在下一章中,您将开始使用 AWS Lambda 控制台并设置您的 Golang 开发环境。

问题

  1. 使用无服务器方法的优势是什么?

  2. Lambda 是一种节省时间的方法吗?

  3. 无服务器架构如何实现微服务?

  4. AWS Lambda 函数的最长时间限制是多少?

  5. 以下哪些是 AWS Lambda 支持的事件源?

  • Amazon Kinesis 数据流

  • Amazon RDS

  • AWS CodeCommit

  • AWS CloudFormation

  1. 解释一下在 Go 中 goroutine 是什么。你如何停止 goroutines?

  2. AWS 中的 Lambda@Edge 是什么?

  3. 函数即服务和平台即服务之间有什么区别?

  4. AWS Lambda 冷启动是什么?

  5. AWS Lambda 函数可以是无状态的还是有状态的?

第二章:开始使用 AWS Lambda

本章提供了在 Go 运行时和开发环境中设置 AWS 环境的指南。您将了解强大的 AWS CLI,它将使部署无服务器应用程序更加高效,并极大地提高您的生产力。

此外,您将获得一组关于如何选择您的 Go 集成开发环境IDE)的提示和建议。

技术要求

在继续安装和配置 AWS 和 Go 环境之前,建议您在笔记本电脑(Windows、Linux 或 macOS X)上跟着本章进行操作,预先安装 Python 2 版本 2.6.5+或 Python 3 版本 3.3+,并设置好 AWS 账户,以便您可以轻松执行给定的命令。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-serverless-Applications-with-Go

设置 AWS 环境

本节将指导您如何安装和配置 AWS 命令行。CLI 是一个可靠且必不可少的工具,将在接下来的章节中介绍;它将通过自动化 Lambda 函数的部署和配置为我们节省大量时间。

AWS 命令行

AWS CLI 是一个强大的工具,可从终端会话中管理您的 AWS 服务和资源。它是建立在 AWS API 之上的,因此通过 AWS 管理控制台可以完成的所有操作都可以通过 CLI 完成;这使它成为一个方便的工具,可以用来通过脚本自动化和控制您的 AWS 基础架构。后面的章节将提供有关使用 CLI 管理 Lambda 函数和创建其他围绕 Lambda 的 AWS 服务的信息。

让我们来看一下 AWS CLI 的安装过程;您可以在AWS 管理控制台部分找到有关其配置和测试的信息。

安装 AWS CLI

要开始,请打开一个新的终端会话,然后使用pip Python 软件包管理器来安装awscli的最新稳定版本:

pip install awscli

如果您已安装 CLI,则建议出于安全目的升级到最新版本:

pip install --upgrade awscli

Windows 用户也可以使用 MSI 安装程序(s3.amazonaws.com/aws-cli/AWSCLI64.msis3.amazonaws.com/aws-cli/AWSCLI32.msi),无需安装 Python。

安装完成后,您需要将 AWS 二进制路径添加到PATH环境变量中,方法如下:

  • 对于 Windows,按 Windows 键,然后键入环境变量。在环境变量窗口中,突出显示系统变量部分中的PATH变量。编辑它并通过在最后一个路径后面放置一个分号来添加路径,输入安装 CLI 二进制文件的文件夹的完整路径。

  • 对于 Linux、Mac 或任何 Unix 系统,请打开您的 shell 配置文件(.bash_profile.profile.bash_login),并将以下行添加到文件的末尾:

export PATH=~/.local/bin:$PATH

最后,将配置文件加载到当前会话中:

source ~/.bash_profile

通过打开一个新的终端会话并输入以下命令来验证 CLI 是否正确安装:

aws --version

您应该能够看到 AWS CLI 的版本;在我的情况下,安装了 1.14.60 版本:

让我们来测试一下,并以法兰克福地区的 Lambda 函数为例进行列出:

aws lambda list-functions --region eu-central-1

上一个命令将显示以下输出:

在使用 CLI 时,通常需要您的 AWS 凭证来对 AWS 服务进行身份验证。有多种方法可以配置 AWS 凭证:

  • 环境凭证AWS_ACCESS_KEY_IDAWS_SECRET_KEY变量。

  • 共享凭证文件~/.aws/credentials文件。

  • IAM 角色:如果您在 EC2 实例中使用 CLI,则这些角色可以避免在生产中管理凭据文件的需要。

在下一节中,我将向您展示如何使用AWS 身份和访问管理IAM)服务为 CLI 创建新用户。

AWS 管理控制台

IAM 是一个允许您管理用户、组以及他们对 AWS 服务的访问级别的服务。

强烈建议您除了进行结算任务外,不要使用 AWS 根帐户执行任何任务,因为它具有创建和删除 IAM 用户、更改结算、关闭帐户以及在 AWS 帐户上执行所有其他操作的最终权限。因此,我们将创建一个新的 IAM 用户,并根据最小权限原则授予其访问正确 AWS 资源所需的权限。在这种情况下,用户将完全访问 AWS Lambda 服务:

  1. 使用您的 AWS 电子邮件地址和密码登录 AWS 管理控制台(console.aws.amazon.com/console/home)。

  2. 安全、身份和合规性部分打开IAM控制台:

  1. 从导航窗格中,选择用户,然后单击“添加用户”按钮,为用户设置名称,并选择编程访问(如果您希望同一用户访问控制台,则还要选择 AWS 管理控制台访问):

  1. 在“设置权限”部分,将 AWSLambdaFullAccess 策略分配给用户:

  1. 在最后一页,您应该看到用户的 AWS 凭据:

确保将访问密钥保存在安全位置,因为您将无法再次看到它们:

配置

我们的 IAM 用户已创建。让我们使用aws configure命令提供访问密钥和秘密密钥以及默认区域:

CLI 将在本地文件~/.aws/credentials(或在 Windows 上的%UserProfile%\.aws/credentials)中存储在前述命令中指定的凭据,内容如下:

[default]
aws_access_key_id = AKIAJYZMNSSSMS4EKH6Q
aws_secret_access_key = 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
region=eu-central-1

测试

就是这样;尝试以下命令,如果您有任何 Lambda 函数,您应该能够看到它们被列出:

默认输出为 JSON。您可以通过添加--output选项(支持的值:jsontabletext)来更改命令的输出格式。以下是以表格格式显示的结果:

此外,您可以使用--query选项从此 JSON 文档中提取输出元素。例如,要输出函数名称属性,可以使用以下命令:

aws lambda list-functions --query Functions[].FunctionName

输出应该类似于以下内容:

可以使用jq这样的工具来操作 JSON。它使我们能够针对 CLI 返回的 JSON 进行过滤、映射、计数和执行其他高级 JSON 处理:

aws lambda list-functions | jq '.Functions[].FunctionName'

控制台将显示以下输出:

设置 Go 环境

本节将指导您如何在多个平台上下载和安装 Go,如何构建一个简单的 Hello World 应用程序,以及如何使用 IDE 加快 Go 开发速度。在此过程中,您将熟悉编写 Go 函数所需的 Go 命令。

运行时环境

从 Go 下载页面(golang.org/dl/)下载适合您操作系统和架构的适当软件包:

  • 对于 macOS X:下载goVersion.darwin.amd64.pkg文件,并按照安装提示进行操作。您可能需要重新启动任何打开的终端会话以使更改生效。

  • 对于 Windows:下载 MSI 安装程序并按照向导进行操作。安装程序将为您设置环境变量。

  • 对于 Linux:打开一个新的终端会话,并键入以下命令(在撰写本文时,当前版本为 1.10):

curl https://golang.org/doc/install?download=go1.10.1.linux-amd64.tar.gz -O /tmp/go1.10.tar.gz
tar -C /usr/local -xzf /tmp/go1.10.tar.gz

前面的命令将使用curl下载最新的 Go 包。然后,它将使用tar来解压该包。接下来,通过将以下行添加到您的 shell 的配置脚本中,将/usr/local/go/bin添加到PATH环境变量中:

export PATH=$PATH:/usr/local/go/bin

如果您将 Go 安装在自定义目录中,而不是/usr/local,您必须设置GOROOT环境变量,指向安装目录:

export GOROOT=PATH/go
export PATH=$PATH:$GOROOT/bin

然后您需要重新加载用户配置文件以应用更改:

$ source ~/.bash_profile

现在 Go 已经正确安装,并且已经为您的计算机设置了路径,让我们来测试一下。创建一个工作区,我们将在整本书中构建我们的无服务器应用程序:

mkdir -p $HOME/go/src

Go 源代码位于工作区中;默认情况下应该是$HOME/go。如果您想使用不同的目录,您需要设置GOPATH环境变量。

要验证 Go 工作区是否正确配置,您可以运行go env命令:

如果设置了GOPATH变量,您就可以开始了。在工作区内,使用vim创建一个main.go文件,内容如下:

package main
import "fmt"

func main(){
  fmt.Println("Welcome to 'Hands-On serverless Applications with Go'")
}

使用以下命令编译文件:

go run main.go

如果成功运行,文件将显示“欢迎来到'使用 Go 进行无服务器应用'”,这表明 Go 正在正确编译文件。

Go 是一种编译语言,因此您可以使用以下命令为应用程序生成单个二进制文件:

go build -o app main.go

如果您想为特定的操作系统和架构构建可执行文件,可以覆盖GOOSGOARCH参数:

GOOS=linux GOARCH=amd64 go build -o app main.go

使用 vim 文本编辑器编辑 Go 并不是最佳选择;因此,在下一节中,我将向您展示如何使用 VSCode 作为 Go 编辑器,以增强您的开发生产力/体验。

开发环境

拥有一个 IDE 可以提高您的开发速度,并节省大量时间,这些时间可以用于调试和搜索正确的语法。此外,您可以轻松导航和搜索 Lambda 函数代码。

但我们应该使用哪一个呢?有许多解决方案;这些解决方案可以分为三个主要类别:

  • IDE:GoLand,Eclipse,Komodo

  • 编辑器:Atom,VSCode,Sublime Text

  • 基于云的 IDE:Cloud9,Codeanywhere,CodeEnvy

Go 生态系统提供了各种编辑器和 IDE;确保您尝试它们,找到最适合您的那个。

我选择了 Visual Studio Code(VS Code),因为它符合我的所有标准:

  • 开源

  • 支持多种语言

  • 插件驱动工具

  • 强大的社区和支持

VSCode 对 Go 开发有很好的支持,包括开箱即用的语法高亮显示,内置的 GIT 集成,所有 Go 工具的集成以及 Delve 调试器。

除了对 Go 的本机支持外,开源社区还构建了一些有用和强大的插件,您可以从 VSCode Marketplace 安装:

VSCode 也是跨平台的,因此您可以在 Mac、Linux 或 Windows 上使用它。使用 Visual Studio Code,您可以通过一系列可用的插件扩展功能,这些插件带来了许多强大和稳健的功能,例如以下内容:

  • 自动完成:在编写 Go 文件时,您可以看到 IntelliSense 提供了建议的完成:

  • 签名帮助:悬停在任何变量、函数或结构上都会给出有关该项的信息,例如文档、签名、预期输入和输出参数。例如,以下屏幕截图显示了有关Println的信息,该信息是从悬停在main.go文件上获得的:

  • 代码格式化:它会在保存时自动格式化您的 Go 源代码,使用gofmt工具,使您的代码更易于编写、阅读和维护。

  • 集成调试器:您可以设置断点和条件断点,并查看每个帧中的堆栈跟踪和本地和全局变量。

  • 自动导入 Go 包:它会在保存时自动导入所需的 Go 包。

  • 测试运行器:它允许您运行、停止和重新启动单元测试以及集成测试。

我期待着 JetBrains 发布的 GoLand 的稳定版本:它看起来是一个非常有前途的 Go IDE,我很期待它的发展。

就是这样!您已经准备好开始在 Go 中构建和部署无服务器应用程序。

摘要

在本章中,我们学习了如何安装、配置和使用 AWS CLI。当涉及管理 AWS 服务和自动部署 Lambda 函数时,这个工具将非常有帮助。然后,我们介绍了如何创建用户并从 IAM 生成 AWS 凭据,以获取最少必要的权限。这样,如果您的访问密钥落入错误的手中,造成的危害将是有限的。此外,我们学习了如何设置 Go 环境,逐步在多个平台(Windows、macOS X 和 Linux)上安装 Go,并编译了我们的第一个 Go 中的 Hello World 应用程序。在此过程中,我们介绍了 Go 中最重要的命令,这将帮助您轻松地跟随后面的章节。

在下一章中,我们将终于动手编写我们的第一个 Go 中的 Lambda 函数。

问题

  1. AWS CLI 不支持哪种格式?
  • JSON

  • XML

  • 文本

  1. 是否建议使用 AWS 根帐户进行日常与 AWS 的交互?如果是,为什么?

  2. 您需要设置哪些环境变量才能使用 AWS CLI?

  3. 如何使用带有命名配置文件的 AWS CLI?

  4. 解释 GOPATH 环境变量。

  5. 哪个命令行命令编译 Go 程序?

  • go build

  • go run

  • go fmt

  • go doc

  1. 什么是 Go 工作区?

第三章:使用 Lambda 开发无服务器函数

在本章中,我们最终将学习如何从头开始编写我们的第一个基于 Go 的 Lambda 函数,然后学习如何手动配置、部署和测试 Lambda 函数。在此过程中,您将获得一组关于如何授予函数访问权限以便安全地与其他 AWS 服务进行交互的提示。

我们将涵盖以下主题:

  • 用 Go 编写 Lambda 函数

  • 执行角色

  • 部署包

  • 事件测试

技术要求

为了跟随本章,您需要按照上一章中描述的设置和配置您的 Go 和 AWS 开发环境。熟悉 Go 是首选但不是必需的。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go

用 Go 编写 Lambda 函数

按照本节中的步骤从头开始创建您的第一个 Go Lambda 函数:

  1. 编写 Lambda 函数需要安装一些依赖项。因此,打开一个新的终端会话,并使用以下命令安装 Go Lambda 包:
go get github.com/aws/aws-lambda-go/lambda
  1. 接下来,打开您喜欢的 Go IDE 或编辑器;在我的情况下,我将使用 VS Code。在GOPATH中创建一个新的项目目录,然后将以下内容粘贴到main.go文件中:
package main

import "github.com/aws/aws-lambda-go/lambda"

func handler() (string, error){
  return "Welcome to Serverless world", nil
}

func main() {
  lambda.Start(handler)
}

前面的代码使用lambda.Start()方法注册一个入口点处理程序,其中包含当调用 Lambda 函数时将执行的代码。Lambda 支持的每种语言都有其自己的要求,用于定义如何定义函数处理程序。对于 Golang,处理程序签名必须满足以下标准:

    • 它必须是一个函数
  • 它可以有 0 到 2 个参数

  • 它必须返回一个错误

  1. 接下来,登录到 AWS 管理控制台(console.aws.amazon.com/console/home)并从“计算”部分选择 Lambda:

  1. 在 AWS Lambda 控制台中,点击“创建函数”按钮,然后按照向导创建您的第一个 Lambda 函数:

  1. 选择从头开始的作者选项,为您的函数命名,然后从支持的语言列表中选择 Go 1.x 作为运行时环境:

您必须为您的 Lambda 函数分配一个 IAM 角色(称为执行角色)。附加到该角色的 IAM 策略定义了您的函数代码被授权与哪些 AWS 服务进行交互。

执行角色

  1. 现在我们已经学会了如何编写我们的第一个 Go Lambda 函数,让我们从身份和访问管理(console.aws.amazon.com/iam/home)中创建一个新的 IAM 角色,以授予函数访问 AWS CloudWatch 日志的权限:

  1. 在权限页面上,您可以选择一个现有的 AWS 托管策略,称为 CloudWatchFullAccess,或者(如第 3 步所示)创建一个最小特权的 IAM 角色(AWS 推荐的第二个选项;专门讨论安全最佳实践的章节将深入讨论 Lambda 函数):

  1. 继续点击“创建策略”按钮,并通过从可视编辑器中选择适当的服务(CloudWatch)来创建一个策略:

  1. 对于熟悉 JSON 格式的读者,可以在 JSON 选项卡中使用 JSON 策略文档。该文档必须有一个声明,授予创建日志组和日志流以及将日志事件上传到 AWS CloudWatch 的权限:
{
 "Version": "2012-10-17",
 "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": [
         "logs:CreateLogStream",
         "logs:CreateLogGroup",
         "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
 }
  1. 在“审阅策略”页面上,为策略输入名称和描述:

  1. 返回“创建角色”页面,点击“刷新”,您应该看到我们之前创建的策略:

  1. 在“审阅”页面上,为角色输入名称并选择“创建角色”:

  1. 现在我们的角色已经定义,返回 Lambda 表单创建并从现有角色下拉列表中选择 IAM 角色(可能需要刷新页面以使更改生效),然后点击“创建函数”按钮:

可以选择使用 AWS CLI 部署 Lambda 函数。有关此内容及其逐步过程的更全面讨论将保留在第六章中,“部署您的无服务器应用程序”中进行。

Lambda 控制台将显示绿色的成功消息,表示您的函数已成功创建:

在编写、打包和创建 Lambda 函数之后,我们有各种配置选项可设置,定义代码在 Lambda 中的执行方式。如前面的截图所示,您可以通过不同的 AWS 服务(称为触发器)触发 Lambda 函数。

将其余高级设置保持不变(VPC、资源使用、版本、别名和并发),因为它们将在后续章节中进行深入讨论。

由于 Go 是最近添加的语言,其开发人员尚未添加内联编辑器的功能,因此您必须以 ZIP 文件格式提供可执行二进制文件,或者引用您已上传包的 S3 存储桶和对象键:

部署包

在本节中,我们将看到如何为函数构建部署包以及如何将其部署到 AWS Lambda 控制台。

上传 ZIP 文件

如第一章“Go 无服务器”中所述,Go 是一种编译语言。因此,您必须使用以下 Shell 脚本生成可执行二进制文件:

#!/bin/bash

echo "Build the binary"
GOOS=linux GOARCH=amd64 go build -o main main.go

echo "Create a ZIP file"
zip deployment.zip main

echo "Cleaning up"
rm main

Lambda 运行时环境基于Amazon Linux AMI;因此,处理程序应为 Linux 编译(注意使用GOOS标志)。

对于 Windows 用户,建议您使用build-lambda-zip工具为 Lambda 创建一个可用的 ZIP 文件。

执行以下 Shell 脚本:

现在我们的 ZIP 文件已经生成;您现在可以返回 Lambda 控制台并上传 ZIP 文件,确保更新处理程序为 main 并保存结果:

处理程序配置属性必须与可执行文件的名称匹配。如果您使用不同名称构建(go build -o NAME)二进制文件,则必须相应地更新处理程序属性。

从 Amazon S3 上传

将部署包上传到 Lambda 的另一种方法是使用 AWS S3 存储桶存储 ZIP 文件。在存储中,选择 S3 打开 Amazon S3 控制台:

在您可以将 ZIP 上传到 Amazon S3 之前,您必须在创建 Lambda 函数的同一 AWS 区域中创建一个新的存储桶,如下截图所示:

S3 存储桶具有全局命名空间。因此,它必须在 Amazon S3 中所有现有存储桶名称中全局唯一。

现在您已经创建了一个存储桶,将在上一节中生成的 ZIP 文件拖放到目标存储桶中,或者使用上传按钮:

可以使用 AWS CLI 将部署包上传到 S3 存储桶,如下所示:

aws s3 cp deployment.zip s3://hello-serverless-packt

确保 IAM 用户被授予S3:PutObject权限,以便使用 AWS 命令行上传对象。

上传后,选择 ZIP 文件并将链接值复制到剪贴板:

返回 Lambda 仪表板,从“代码输入类型”下拉列表中选择“从 Amazon S3 上传文件”,然后粘贴 S3 中部署包的路径:

保存后,您可以在 AWS Lambda 控制台中测试 Lambda 函数。

事件测试

以下步骤将演示如何从控制台调用 Lambda 函数:

  1. 现在函数已部署,让我们通过单击控制台右上角的“测试”按钮,手动使用示例事件数据来调用它。

  2. 选择“配置测试事件”会打开一个新窗口,其中有一个下拉菜单。下拉菜单中的项目是样本 JSON 事件模板,这些模板是 Lambda 可以消耗的源事件或触发器的模拟,以便测试其功能(回顾第一章,Go Serverless):

  1. 保留默认的 Hello World 选项。输入事件名称并提供一个空的 JSON 对象:

  1. 选择创建。保存后,您应该在测试列表中看到 EmptyInput:

  1. 再次单击“测试”按钮。AWS Lambda 将执行您的函数并显示以下输出:

除了函数返回的结果外,我们还将能够看到“欢迎来到无服务器世界”,这是关于 Lambda 函数的资源使用和执行持续时间的全局概述,以及 Lambda 函数写入 CloudWatch 的日志。

将在第十一章中讨论使用 CloudWatch 指标进行高级监控以及使用 CloudWatch 日志和 CloudTrail 进行日志记录和故障排除。

恭喜!您刚刚设置并部署了您的第一个 Lambda 函数。当您使用触发器或源事件与 Lambda 函数一起使用时,Lambda 的真正力量就会显现出来,因此它会根据发生的事件执行。我们将在下一章中看看这一点。

摘要

在本章中,我们学习了如何从头开始使用 Go 编写 Lambda 函数。然后,我们介绍了如何为 Lambda 创建执行角色,以便将事件日志生成到 AWS CloudWatch。我们还学习了如何从 AWS Lambda 控制台手动测试和调用此函数。

在下一章中,我将向您介绍如何使用触发器自动调用 Lambda 函数,以及如何使用 AWS API Gateway 构建一个统一的 RESTful API 来执行 Lambda 函数以响应 HTTP 请求。

问题

  1. 为 AWS Lambda 函数创建 IAM 角色的命令行命令是什么?

  2. 在弗吉尼亚地区(us-east-1)创建一个新的 S3 存储桶并将 Lambda 部署包上传到其中的命令行命令是什么?

  3. Lambda 包大小限制是多少?

  • 10 MB

  • 50 MB

  • 250 MB

  1. AWS Lambda 控制台支持编辑 Go 源代码。
  • True

  • False

  1. AWS Lambda 执行环境的基础是什么?
  • Amazon Linux 镜像

  • Microsoft Windows Server

  1. AWS Lambda 中如何表示事件?

第四章:使用 API 网关设置 API 端点

在上一章中,我们学习了如何使用 Go 构建我们的第一个 Lambda 函数。我们还学习了如何从控制台手动调用它。为了利用 Lambda 的强大功能,在本章中,我们将学习如何在收到 HTTP 请求时触发这个 Lambda 函数(事件驱动架构)使用 AWS API 网关服务。在本章结束时,您将熟悉 API 网关高级主题,如资源、部署阶段、调试等。

我们将涵盖以下主题:

  • 开始使用 API 网关

  • 构建 RESTful API

技术要求

本章是上一章的后续内容,因此建议先阅读上一章,以便轻松地理解本部分。此外,需要对 RESTful API 设计和实践有基本的了解。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go

开始使用 API 网关

API 网关是 AWS 无服务器 API 代理服务,允许您为所有 Lambda 函数创建一个单一且统一的入口点。它代理和路由传入的 HTTP 请求到适当的 Lambda 函数(映射)。从服务器端的角度来看,它是一个外观或包装器,位于 Lambda 函数的顶部。但是,从客户端的角度来看,它只是一个单一的单片应用程序。

除了为客户端提供单一接口和可伸缩性外,API 网关还提供了以下强大功能:

  • 缓存:您可以缓存端点响应,从而减少对 Lambda 函数的请求次数(成本优化)并增强响应时间。

  • CORS 配置:默认情况下,浏览器拒绝从不同域的资源访问。可以通过在 API 网关中启用跨域资源共享CORS)来覆盖此策略。

CORS 将在第九章中深入讨论,使用 S3 构建前端,并提供一个实际示例。

  • 部署阶段/生命周期:您可以管理和维护多个 API 版本和环境(沙盒、QA、暂存和生产)。

  • 监控:通过启用与 API 网关的 CloudWatch 集成,简化故障排除和调试传入请求和传出响应。它将推送一系列日志事件到 AWS CloudWatch 日志,并且您可以向 CloudWatch 公开一组指标,包括:

  • 客户端错误,包括 4XX 和 5XX 状态代码

  • 在给定周期内的 API 请求总数

  • 端点响应时间(延迟)

  • 可视化编辑:您可以直接从控制台描述 API 资源和方法,而无需任何编码或 RESTful API 知识。

  • 文档:您可以为 API 的每个版本生成 API 文档,并具有导入/导出和发布文档到 Swagger 规范的能力。

  • 安全和身份验证:您可以使用 IAM 角色和策略保护您的 RESTful API 端点。API 网关还可以充当防火墙,防止 DDoS 攻击和 SQL/脚本注入。此外,可以在此级别强制执行速率限制或节流。

以上是足够的理论。在下一节中,我们将介绍如何设置 API 网关以在收到 HTTP 请求时触发我们的 Lambda 函数。

除了支持 AWS Lambda 外,API 网关还可用于响应 HTTP 请求调用其他 AWS 服务(EC2、S3、Kinesis、CloudFront 等)或外部 HTTP 端点。

设置 API 端点

以下部分描述了如何使用 API 网关触发 Lambda 函数:

  1. 要设置 API 端点,请登录到AWS 管理控制台console.aws.amazon.com/console/home),转到 AWS Lambda 控制台,并选择我们在上一章节中构建的 Lambda 函数 HelloServerless:

  1. 从可用触发器列表中搜索 API 网关并单击它:

可用触发器的列表可能会根据您使用的 AWS 区域而变化,因为 AWS Lambda 支持的源事件并不在所有 AWS 区域都可用。

  1. 页面底部将显示一个“配置触发器”部分,如下面的屏幕截图所示:

  1. 创建一个新的 API,为其命名,将部署阶段设置为staging,并将 API 公开给公众:

表格将需要填写以下参数:

  • API 名称:API 的唯一标识符。

  • 部署阶段:API 阶段环境,有助于分隔和维护不同的 API 环境(开发、staging、生产等)和版本/发布(主要、次要、测试等)。此外,如果实施了持续集成/持续部署流水线,它非常方便。

  • 安全性:它定义了 API 端点是公开还是私有:

  • 开放:可公开访问,任何人都可以调用

  • AWS IAM:将由被授予 IAM 权限的用户调用

  • 使用访问密钥打开:需要 AWS 访问密钥才能调用

  1. 定义 API 后,将显示以下部分:

  1. 单击页面顶部的“保存”按钮以创建 API 网关触发器。保存后,API 网关调用 URL 将以以下格式生成:https://API_ID.execute-api.AWS_REGION.amazonaws.com/DEPLOYMENT_STAGE/FUNCTION_NAME,如下面的屏幕截图所示:

  1. 使用 API 调用 URL 在您喜欢的浏览器中打开,您应该会看到如下屏幕截图中所示的消息:

  1. 内部服务器错误消息意味着 Lambda 方面出现了问题。为了帮助我们解决问题并调试问题,我们将在 API 网关中启用日志记录功能。

调试和故障排除

为了解决 API 网关服务器错误,我们需要按照以下步骤启用日志记录:

  1. 首先,我们需要授予 API 网关访问 CloudWatch 的权限,以便能够将 API 网关日志事件推送到 CloudWatch 日志中。因此,我们需要从身份和访问管理中创建一个新的 IAM 角色。

为了避免重复,有些部分已被跳过。如果您需要逐步操作,请确保您已经从上一章节开始。

下面的屏幕截图将让您了解如何创建 IAM 角色:

  1. 从 AWS 服务列表中选择 API 网关,然后在权限页面上,您可以执行以下操作之一:
  • 选择一个名为 AmazonAPIGatewayPushToCloudWatchLogs 的现有策略,如下面的屏幕截图所示:

    • 创建一个新的策略文档,其中包含以下 JSON:
{
 "Version": "2012-10-17",
 "Statement": [
 {
 "Effect": "Allow",
     "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:DescribeLogGroups",
        "logs:DescribeLogStreams",
        "logs:PutLogEvents",
        "logs:GetLogEvents",
        "logs:FilterLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
  1. 接下来,为角色指定一个名称,并将角色 ARN(Amazon 资源名称)复制到剪贴板上:

  1. 然后,从“网络和内容传递”部分选择 API 网关。单击“设置”,粘贴我们之前创建的 IAM 角色 ARN:

  1. 保存并选择由 Lambda 函数创建的 API。在导航窗格中单击“阶段”:

  1. 然后,点击日志选项卡,在 CloudWatch 设置下,点击启用 CloudWatch 日志,并选择要捕获的日志级别。在这种情况下,我们对错误日志感兴趣:

  1. 尝试使用 API URL 再次调用 Lambda,并跳转到 AWS CloudWatch 日志控制台;您会看到已创建了一个新的日志组,格式为API-Gateway-Execution-Logs_AP_ID/DEPLOYMENT_STAGE

  1. 点击日志组,您将看到 API 网关生成的日志流:

  1. 前面的日志表明,从 Lambda 函数返回的响应格式不正确。正确的响应格式应包含以下属性:
  • Body:这是一个必需的属性,包含函数的实际输出。

  • 状态码:这是函数响应状态码,如 HTTP/1.1 标准中所述(tools.ietf.org/html/rfc7231#section-6)。这是强制性的,否则 API 网关将显示 5XX 错误,如前一节所示。

  • 可选参数:它包括HeadersIsBase64Encoded等内容。

在接下来的部分中,我们将通过格式化 Lambda 函数返回的响应来修复此错误响应,以满足 API 网关期望的格式。

使用 HTTP 请求调用函数

如前一节所示,我们需要修复 Lambda 函数返回的响应。我们将返回一个包含实际字符串值的struct变量,以及一个StatusCode,其值为200,告诉 API 网关请求成功。为此,更新main.go文件以匹配以下签名:

package main

import "github.com/aws/aws-lambda-go/lambda"

type Response struct {    
  StatusCode int `json:"statusCode"`
  Body string `json:"body"`
}

func handler() (Response, error) {
  return Response{
    StatusCode: 200,
    Body: "Welcome to Serverless world",
  }
, nil
}

func main() {
  lambda.Start(handler)
} 

更新后,使用上一章节提供的 Shell 脚本构建部署包,并使用 AWS Lambda 控制台上传包到 Lambda,或使用以下 AWS CLI 命令:

aws lambda update-function-code --function-name HelloServerless \
 --zip-file fileb://./deployment.zip \
 --region us-east-1

确保您授予 IAM 用户lambda:CreateFunctionlambda:UpdateFunctionCode权限,以便在本章节中使用 AWS 命令行。

返回到您的网络浏览器,并再次调用 API 网关 URL:

恭喜!您刚刚使用 Lambda 和 API 网关构建了您的第一个事件驱动函数。

供快速参考,Lambda Go 包提供了一种更容易地将 Lambda 与 API 网关集成的方法,即使用APIGatewayProxyResponse结构。

package main

import (
  "github.com/aws/aws-lambda-go/events"
  "github.com/aws/aws-lambda-go/lambda"
)

func handler() (events.APIGatewayProxyResponse, error) {
  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Body: "Welcome to Serverless world",
  }, nil
}

func main() {
  lambda.Start(handler)
}

现在我们知道如何在响应 HTTP 请求时调用 Lambda 函数,让我们进一步构建一个带有 API 网关的 RESTful API。

构建 RESTful API

在本节中,我们将从头开始设计、构建和部署一个 RESTful API,以探索涉及 Lambda 和 API 网关的一些高级主题。

API 架构

在进一步详细介绍架构之前,我们将看一下一个 API,它将帮助本地电影租赁店管理其可用电影。以下图表显示了 API 网关和 Lambda 如何适应 API 架构:

AWS Lambda 赋予了微服务开发的能力。也就是说,每个端点触发不同的 Lambda 函数。这些函数彼此独立,可以用不同的语言编写。因此,这导致了在函数级别的扩展、更容易的单元测试和松散的耦合。

所有来自客户端的请求首先经过 API 网关。然后将传入的请求相应地路由到正确的 Lambda 函数。

请注意,单个 Lambda 函数可以处理多个 HTTP 方法(GETPOSTPUTDELETE等)。为了利用微服务的优势,我们将为每个功能创建多个 Lambda 函数。但是,构建一个单一的 Lambda 函数来处理多个端点可能是一个很好的练习。

端点设计

现在架构已经定义好了,我们将实现前面图表中描述的功能。

GET 方法

要实现的第一个功能是列出电影。这就是GET方法发挥作用的地方。要执行此操作,需要参考以下步骤:

  1. 创建一个 Lambda 函数来注册findAll处理程序。此处理程序将movies结构的列表转换为string,然后将此字符串包装在APIGatewayProxyResponse变量中,并返回带有 200 HTTP 状态代码的字符串。它还处理转换失败的错误。处理程序的实现如下:
package main

import (
  "encoding/json"

  "github.com/aws/aws-lambda-go/events"
  "github.com/aws/aws-lambda-go/lambda"
)

var movies = []struct {
  ID int `json:"id"`
  Name string `json:"name"`
}{
    {
      ID: 1,
      Name: "Avengers",
    },
    {
      ID: 2,
      Name: "Ant-Man",
    },
    {
      ID: 3,
      Name: "Thor",
    },
    {
      ID: 4,
      Name: "Hulk",
    }, {
      ID: 5,
      Name: "Doctor Strange",
    },
}

func findAll() (events.APIGatewayProxyResponse, error) {
  response, err := json.Marshal(movies)
  if err != nil {
    return events.APIGatewayProxyResponse{}, err
  }

  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
    },
    Body: string(response),
  }, nil
}

func main() {
  lambda.Start(findAll)
}

您可以使用net/http Go 包而不是硬编码 HTTP 状态代码,并使用内置的状态代码变量,如http.StatusOKhttp.StatusCreatedhttp.StatusBadRequesthttp.StatusInternalServerError等。

  1. 然后,在构建 ZIP 文件后,使用 AWS CLI 创建一个新的 Lambda 函数:
aws lambda create-function --function-name FindAllMovies \
 --zip-file fileb://./deployment.zip \
 --runtime go1.x --handler main \
 --role arn:aws:iam::ACCOUNT_ID:role/FindAllMoviesRole \
 --region us-east-1

FindAllMoviesRole应该事先创建,如前一章所述,具有允许流式传输 Lambda 日志到 AWS CloudWatch 的权限。

  1. 返回 AWS Lambda 控制台;您应该看到函数已成功创建:

  1. 创建一个带有空 JSON 的示例事件,因为该函数不需要任何参数,并单击“测试”按钮:

您会注意到在前一个屏幕截图中,该函数以 JSON 格式返回了预期的输出。

  1. 现在函数已经定义好了,我们需要创建一个新的 API 网关来触发它:

  1. 接下来,从“操作”下拉列表中选择“创建资源”,并将其命名为 movies:

  1. 通过单击“创建方法”在/movies资源上公开一个 GET 方法。在“集成类型”部分下选择 Lambda 函数,并选择FindAllMovies函数:

  1. 要部署 API,请从“操作”下拉列表中选择“部署 API”。您将被提示创建一个新的部署阶段:

  1. 创建部署阶段后,将显示一个调用 URL:

  1. 将浏览器指向给定的 URL,或者使用像 Postman 或 Insomnia 这样的现代 REST 客户端。我选择使用 cURL 工具,因为它默认安装在几乎所有操作系统上:
curl -sX GET https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'

上述命令将以 JSON 格式返回电影列表:

当调用GET端点时,请求将通过 API 网关,触发findAll处理程序。这将返回一个以 JSON 格式代理给客户端的响应。

现在findAll函数已经部署,我们可以实现一个findOne函数来按其 ID 搜索电影。

带参数的 GET 方法

findOne处理程序期望包含事件输入的APIGatewayProxyRequest参数。然后,它使用PathParameters方法获取电影 ID 并验证它。如果提供的 ID 不是有效数字,则Atoi方法将返回错误,并将 500 错误代码返回给客户端。否则,将根据索引获取电影,并以包含APIGatewayProxyResponse的 200 OK 状态返回给客户端:

...

func findOne(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  id, err := strconv.Atoi(req.PathParameters["id"])
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: 500,
      Body:       "ID must be a number",
    }, nil
  }

  response, err := json.Marshal(movies[id-1])
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: 500,
      Body:       err.Error(),
    }, nil
  }

  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
    },
    Body: string(response),
  }, nil
}

func main() {
  lambda.Start(findOne)
}

请注意,在上述代码中,我们使用了处理错误的两种方法。第一种是err.Error()方法,当编码失败时返回内置的 Go 错误消息。第二种是用户定义的错误,它是特定于错误的,易于从客户端的角度理解和调试。

类似于FindAllMovies函数,为搜索电影创建一个新的 Lambda 函数:

aws lambda create-function --function-name FindOneMovie \
 --zip-file fileb://./deployment.zip \
 --runtime go1.x --handler main \
 --role arn:aws:iam::ACCOUNT_ID:role/FindOneMovieRole \
 --region us-east-1

返回 API Gateway 控制台,创建一个新资源,并公开GET方法,然后将资源链接到FindOneMovie函数。请注意路径中的{id}占位符的使用。id的值将通过APIGatewayProxyResponse对象提供。以下屏幕截图描述了这一点:

重新部署 API,并使用以下 cURL 命令测试端点:

curl -sX https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies/1 | jq '.' 

将返回以下 JSON:

当使用 ID 调用 API URL 时,如果存在,将返回与 ID 对应的电影。

POST 方法

现在我们知道了如何使用路径参数和不使用路径参数来使用 GET 方法。下一步将是通过 API Gateway 向 Lambda 函数传递 JSON 有效负载。代码是不言自明的。它将请求输入转换为电影结构,将其添加到电影列表中,并以 JSON 格式返回新的电影列表:

package main

import (
  "encoding/json"

  "github.com/aws/aws-lambda-go/events"
  "github.com/aws/aws-lambda-go/lambda"
)

type Movie struct {
  ID int `json:"id"`
  Name string `json:"name"`
}

var movies = []Movie{
  Movie{
    ID: 1,
    Name: "Avengers",
  },
  ...
}

func insert(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  var movie Movie
  err := json.Unmarshal([]byte(req.Body), &movie)
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: 400,
      Body: "Invalid payload",
    }, nil
  }

  movies = append(movies, movie)

  response, err := json.Marshal(movies)
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: 500,
      Body: err.Error(),
    }, nil
  }

  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
    },
    Body: string(response),
  }, nil
}

func main() {
  lambda.Start(insert)
}

接下来,使用以下命令为InsertMovie创建一个新的 Lambda 函数

aws lambda create-function --function-name InsertMovie \
 --zip-file fileb://./deployment.zip \
 --runtime go1.x --handler main \
 --role arn:aws:iam::ACCOUNT_ID:role/InsertMovieRole \
 --region us-east-1

接下来,在/movies资源上创建一个POST方法,并将其链接到InsertMovie函数:

要测试它,使用以下 cURL 命令,使用POST动词和-d标志,后跟 JSON 字符串(带有idname属性):

curl -sX POST -d '{"id":6, "name": "Spiderman:Homecoming"}' https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'

上述命令将返回以下 JSON 响应:

如您所见,新电影已成功插入。如果再次测试,它应该按预期工作:

curl -sX POST -d '{"id":7, "name": "Iron man"}' https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'

上述命令将返回以下 JSON 响应:

如您所见,它成功了,并且电影再次按预期插入,但是如果我们等待几分钟并尝试插入第三部电影会怎样?以下命令将用于再次执行它:

curl -sX POST -d '{"id":8, "name": "Captain America"}' https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'

再次,将返回一个新的 JSON 响应:

您会发现 ID 为 6 和 7 的电影已被移除;为什么会这样?很简单。如果您还记得第一章中的Go Serverless,Lambda 函数是无状态的。当第一次调用InsertMovie函数(第一次插入)时,AWS Lambda 会创建一个容器并将函数有效负载部署到容器中。然后,在被终止之前保持活动状态几分钟(热启动),这就解释了为什么第二次插入会成功。在第三次插入中,容器已经被终止,因此 Lambda 会创建一个新的容器(冷启动)来处理插入。

因此,之前的状态已丢失。以下图表说明了冷/热启动问题:

这解释了为什么 Lambda 函数应该是无状态的,以及为什么我们不应该假设状态会从一次调用到下一次调用中保留。那么,在处理无服务器应用程序时,我们如何管理数据持久性呢?答案是使用 DynamoDB 等外部数据库,这将是即将到来的章节的主题。

总结

在本章中,您学习了如何使用 Lambda 和 API Gateway 从头开始构建 RESTful API。我们还介绍了如何通过启用 CloudWatch 日志功能来调试和解决传入的 API Gateway 请求,以及如何创建 API 部署阶段以及如何创建具有不同 HTTP 方法的多个端点。最后,我们了解了冷/热容器问题以及为什么 Lambda 函数应该是无状态的。

在接下来的章节中,我们将使用 DynamoDB 作为数据库,为我们的 API 管理数据持久性。

第五章:使用 DynamoDB 管理数据持久性

在上一章中,我们学习了如何使用 Lambda 和 API Gateway 构建 RESTful API,并发现了为什么 Lambda 函数应该是无状态的。在本章中,我们将使用 AWS DynamoDB 解决无状态问题。此外,我们还将看到如何将其与 Lambda 函数集成。

我们将涵盖以下主题:

  • 设置 DynamoDB

  • 使用 DynamoDB

技术要求

本章是上一章的后续,因为它将使用相同的源代码。因此,为避免重复,某些代码片段将不予解释。此外,最好具备 NoSQL 概念的基本知识,以便您可以轻松地跟随本章。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go

设置 DynamoDB

DynamoDB 是 AWS 的 NoSQL 数据库。它是一个托管的 AWS 服务,允许您在不管理或维护数据库服务器的情况下按比例存储和检索数据。

在深入了解与 AWS Lambda 的集成之前,您需要了解一些关于 DynamoDB 的关键概念:

  • 结构和设计

  • :这是一组项目(行),其中每个项目都是一组属性(列)和值。

  • 分区键:也称为哈希键。这是 DynamoDB 用来确定可以找到项目的分区(物理位置)(读操作)或将存储项目的分区(写操作)的唯一 ID。可以使用排序键来对同一分区中的项目进行排序。

  • 索引:与关系数据库类似,索引用于加速查询。在 DynamoDB 中,可以创建两种类型的索引:

  • 全局二级索引GSI

  • 本地二级索引LSI

  • 操作

  • 扫描:顾名思义,此操作在返回所请求的项目之前扫描整个表。

  • 查询:此操作根据主键值查找项目。

  • PutItem:这将创建一个新项目或用新项目替换旧项目。

  • GetItem:通过其主键查找项目。

  • DeleteItem:通过其主键在表中删除单个项目。

在性能方面,扫描操作效率较低,成本较高(消耗更多吞吐量),因为该操作必须遍历表中的每个项目以获取所请求的项目。因此,始终建议使用查询而不是扫描操作。

现在您熟悉了 DynamoDB 的术语,我们可以开始创建我们的第一个 DynamoDB 表来存储 API 项目。

创建表

要开始创建表,请登录 AWS 管理控制台(console.aws.amazon.com/console/home)并从数据库部分选择 DynamoDB。点击创建表按钮以创建新的 DynamoDB 表,如下面的屏幕截图所示:

接下来,在下一个示例中为表命名为movies。由于每部电影将由唯一 ID 标识,因此它将是表的分区键。将所有其他设置保留为默认状态,然后点击创建,如下所示:

等待几秒钟,直到表被创建,如下所示:

创建movies表后,将提示成功消息以确认其创建。现在,我们需要将示例数据加载到表中。

加载示例数据

要在movies表中填充项目,请点击项目选项卡:

然后,点击创建项目并插入一个新电影,如下面的屏幕截图所示(您需要使用加号(+)按钮添加额外的列来存储电影名称):

点击保存。表应该看起来像这样:

对于真实的应用程序,我们不会使用控制台来填充数百万条目。为了节省时间,我们将使用 AWS SDK 编写一个小型的 Go 应用程序来将项目加载到表中。

在 Go 工作区中创建一个新项目,并将以下内容复制到init-db.go文件中:

func main() {
  cfg, err := external.LoadDefaultAWSConfig()
  if err != nil {
    log.Fatal(err)
  }

  movies, err := readMovies("movies.json")
  if err != nil {
    log.Fatal(err)
  }

  for _, movie := range movies {
    fmt.Println("Inserting:", movie.Name)
    err = insertMovie(cfg, movie)
    if err != nil {
      log.Fatal(err)
    }
  }

}

上述代码读取一个 JSON 文件(github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go/blob/master/ch5/movies.json),其中包含一系列电影;将其编码为Movie结构的数组,如下所示:

func readMovies(fileName string) ([]Movie, error) {
  movies := make([]Movie, 0)

  data, err := ioutil.ReadFile(fileName)
  if err != nil {
    return movies, err
  }

  err = json.Unmarshal(data, &movies)
  if err != nil {
    return movies, err
  }

  return movies, nil
}

然后,它遍历电影数组中的每部电影。然后,使用PutItem方法将其插入 DynamoDB 表中,如下所示:

func insertMovie(cfg aws.Config, movie Movie) error {
  item, err := dynamodbattribute.MarshalMap(movie)
  if err != nil {
    return err
  }

  svc := dynamodb.New(cfg)
  req := svc.PutItemRequest(&dynamodb.PutItemInput{
    TableName: aws.String("movies"),
    Item: item,
  })
  _, err = req.Send()
  if err != nil {
    return err
  }
  return nil
}

确保使用终端会话中的go get github.com/aws/aws-sdk-go-v2/aws命令安装 AWS Go SDK.

要加载movies表中的数据,请输入以下命令:

AWS_REGION=us-east-1 go run init-db.go

您可以使用 DynamoDB 控制台验证加载到movies表中的数据,如下截图所示:

现在 DynamoDB 表已准备就绪,我们需要更新每个 API 端点函数的代码,以使用表而不是硬编码的电影列表。

使用 DynamoDB

在这一部分,我们将更新现有的函数,从 DynamoDB 表中读取和写入。以下图表描述了目标架构:

API Gateway 将转发传入的请求到目标 Lambda 函数,该函数将在movies表上调用相应的 DynamoDB 操作。

扫描请求

要开始,我们需要实现负责返回电影列表的函数;以下步骤描述了如何实现这一点:

  1. 更新findAll处理程序端点以使用Scan方法从表中获取所有项目:
func findAll() (events.APIGatewayProxyResponse, error) {
  cfg, err := external.LoadDefaultAWSConfig()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while retrieving AWS credentials",
    }, nil
  }

  svc := dynamodb.New(cfg)
  req := svc.ScanRequest(&dynamodb.ScanInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
  })
  res, err := req.Send()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while scanning DynamoDB",
    }, nil
  }

  response, err := json.Marshal(res.Items)
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while decoding to string value",
    }, nil
  }

  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
    },
    Body: string(response),
  }, nil
}

此功能的完整实现可以在 GitHub 存储库中找到(github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go/blob/master/ch5/findAll/main.go)。

  1. 构建部署包,并使用以下 AWS CLI 命令更新FindAllMovies Lambda 函数代码:
aws lambda update-function-code --function-name FindAllMovies \
 --zip-file fileb://./deployment.zip \
 --region us-east-1
  1. 确保更新 FindAllMoviesRole,以授予 Lambda 函数调用 DynamoDB 表上的Scan操作的权限,方法是添加以下 IAM 策略:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "1",
      "Effect": "Allow",
      "Action": "dynamodb:Scan",
      "Resource": [
        "arn:aws:dynamodb:us-east-1:ACCOUNT_ID:table/movies/index/ID",
        "arn:aws:dynamodb:us-east-1:ACCOUNT_ID:table/movies"
      ]
    }
  ]
}

一旦策略分配给 IAM 角色,它应该成为附加策略的一部分,如下一张截图所示:

  1. 最后,使用 Lambda 控制台或 AWS CLI,添加一个新的环境变量,指向我们之前创建的 DynamoDB 表名:
aws lambda update-function-configuration --function-name FindAllMovies \
 --environment Variables={TABLE_NAME=movies} \
 --region us-east-1

下图显示了一个正确配置的 FindAllMovies 函数,具有对 DynamoDB 和 CloudWatch 的 IAM 访问权限,并具有定义的TABLE_NAME环境变量:

正确配置的 FindAllMovies 函数

  1. 保存并使用以下 cURL 命令调用 API Gateway URL:
curl -sX GET https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'
  1. 将以 JSON 格式返回一个数组,如下所示:

  1. 端点正在工作,并从表中获取电影项目,但返回的 JSON 是原始的 DynamoDB 响应。我们将通过仅返回IDName属性来修复这个问题,如下所示:
movies := make([]Movie, 0)
for _, item := range res.Items {
  movies = append(movies, Movie{
    ID: *item["ID"].S,
    Name: *item["Name"].S,
  })
}

response, err := json.Marshal(movies)
  1. 此外,生成 ZIP 文件并更新 Lambda 函数代码,然后使用前面给出的 cURL 命令调用 API Gateway URL,如下所示:

好多了,对吧?

GetItem 请求

要实现的第二个功能将负责从 DynamoDB 返回单个项目,以下步骤说明了应该如何构建它:

  1. 更新findOne处理程序以调用 DynamoDB 中的GetItem方法。这应该返回一个带有传递给 API 端点参数的标识符的单个项目:
func findOne(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  id := request.PathParameters["id"]

  cfg, err := external.LoadDefaultAWSConfig()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while retrieving AWS credentials",
    }, nil
  }

  svc := dynamodb.New(cfg)
  req := svc.GetItemRequest(&dynamodb.GetItemInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
    Key: map[string]dynamodb.AttributeValue{
      "ID": dynamodb.AttributeValue{
        S: aws.String(id),
      },
    },
  })
  res, err := req.Send()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while fetching movie from DynamoDB",
    }, nil
  }

  ...
}

此函数的完整实现可以在 GitHub 存储库中找到(github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go/blob/master/ch5/findOne/main.go)。

  1. FindAllMovies函数类似,创建一个 ZIP 文件,并使用以下 AWS CLI 命令更新现有的 Lambda 函数代码:
aws lambda update-function-code --function-name FindOneMovie \
 --zip-file fileb://./deployment.zip \
 --region us-east-1
  1. 授予FindOneMovie Lambda 函数对movies表的GetItem权限的 IAM 策略:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "1",
      "Effect": "Allow",
      "Action": "dynamodb:GetItem",
      "Resource": "arn:aws:dynamodb:us-east-1:ACCOUNT_ID:table/movies"
    }
  ]
}
  1. IAM 角色应配置如下截图所示:

  1. 使用 DynamoDB 表名定义一个新的环境变量:
aws lambda update-function-configuration --function-name FindOneMovie \
 --environment Variables={TABLE_NAME=movies} \
 --region us-east-1
  1. 返回FindOneMovie仪表板,并验证所有设置是否已配置,如下截图所示:

  1. 通过发出以下 cURL 命令调用 API Gateway:
curl -sX GET https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies/3 | jq '.'
  1. 如预期的那样,响应是一个具有 ID 为 3 的单个电影项目,如 cURL 命令中请求的那样:

PutItem 请求

到目前为止,我们已经学会了如何列出所有项目并从 DynamoDB 返回单个项目。以下部分描述了如何实现 Lambda 函数以将新项目添加到数据库中:

  1. 更新insert处理程序以调用PutItem方法将新电影插入表中:
func insert(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  ...

  cfg, err := external.LoadDefaultAWSConfig()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while retrieving AWS credentials",
    }, nil
  }

  svc := dynamodb.New(cfg)
  req := svc.PutItemRequest(&dynamodb.PutItemInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
    Item: map[string]dynamodb.AttributeValue{
      "ID": dynamodb.AttributeValue{
        S: aws.String(movie.ID),
      },
      "Name": dynamodb.AttributeValue{
        S: aws.String(movie.Name),
      },
    },
  })
  _, err = req.Send()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while inserting movie to DynamoDB",
    }, nil
  }

  ...
}

此函数的完整实现可以在 GitHub 存储库中找到(github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go/blob/master/ch5/insert/main.go)。

  1. 创建部署包,并使用以下命令更新InsertMovie Lambda 函数代码:
aws lambda update-function-code --function-name InsertMovie \
 --zip-file fileb://./deployment.zip \
 --region us-east-1
  1. 允许该函数在电影表上调用PutItem操作,并使用以下 IAM 策略:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "1",
      "Effect": "Allow",
      "Action": "dynamodb:PutItem",
      "Resource": "arn:aws:dynamodb:us-east-1:ACCOUNT_ID:table/movies"
    }
  ]
}

以下截图显示 IAM 角色已更新以处理PutItem操作的权限:

  1. 创建一个新的环境变量,DynamoDB 表名如下:
aws lambda update-function-configuration --function-name InsertMovie \
 --environment Variables={TABLE_NAME=movies} \
 --region us-east-1
  1. 确保 Lambda 函数配置如下:

正确配置的 InsertMovie 函数

  1. 通过在 API Gateway URL 上调用以下 cURL 命令插入新电影:
curl -sX POST -d '{"id":"17", "name":"The Punisher"}' https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'
  1. 验证电影是否已插入 DynamoDB 控制台,如下截图所示:

验证插入是否成功执行的另一种方法是使用 cURL 命令使用findAll端点:

curl -sX GET https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'
  1. 具有 ID 为17的电影已创建。如果表中包含具有相同 ID 的电影项目,则会被替换。以下是输出:

DeleteItem 请求

最后,为了从 DynamoDB 中删除项目,应实现以下 Lambda 函数:

  1. 注册一个新的处理程序来删除电影。处理程序将请求体中的有效负载编码为Movie结构:
var movie Movie
err := json.Unmarshal([]byte(request.Body), &movie)
if err != nil {
   return events.APIGatewayProxyResponse{
      StatusCode: 400,
      Body: "Invalid payload",
   }, nil
}
  1. 然后,调用DeleteItem方法,并将电影 ID 作为参数从表中删除:
cfg, err := external.LoadDefaultAWSConfig()
if err != nil {
  return events.APIGatewayProxyResponse{
    StatusCode: http.StatusInternalServerError,
    Body: "Error while retrieving AWS credentials",
  }, nil
}

svc := dynamodb.New(cfg)
req := svc.DeleteItemRequest(&dynamodb.DeleteItemInput{
  TableName: aws.String(os.Getenv("TABLE_NAME")),
  Key: map[string]dynamodb.AttributeValue{
    "ID": dynamodb.AttributeValue{
      S: aws.String(movie.ID),
    },
  },
})
_, err = req.Send()
if err != nil {
  return events.APIGatewayProxyResponse{
    StatusCode: http.StatusInternalServerError,
    Body: "Error while deleting movie from DynamoDB",
  }, nil
}

此函数的完整实现可以在 GitHub 存储库中找到(github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go/blob/master/ch5/delete/main.go)。

  1. 与其他函数一样,创建一个名为DeleteMovieRole的新 IAM 角色,该角色具有将日志推送到 CloudWatch 并在电影表上调用DeleteItem操作的权限,如下截图所示:

  1. 接下来,在构建部署包后创建一个新的 Lambda 函数:
aws lambda create-function --function-name DeleteMovie \
 --zip-file fileb://./deployment.zip \
 --runtime go1.x --handler main \
 --role arn:aws:iam::ACCOUNT_ID:role/DeleteMovieRole \
 --environment Variables={TABLE_NAME=movies} \
 --region us-east-1
  1. 返回 Lambda 控制台。应该已创建一个DeleteMovie函数,如下截图所示:

  1. 最后,我们需要在 API Gateway 的/movies端点上公开一个DELETE方法。为此,我们不会使用 API Gateway 控制台,而是使用 AWS CLI,以便您熟悉它。

  2. 要在movies资源上创建一个DELETE方法,我们将使用以下命令:

aws apigateway put-method --rest-api-id API_ID \
 --resource-id RESOURCE_ID \
 --http-method DELETE \
 --authorization-type "NONE" \
 --region us-east-1 
  1. 但是,我们需要提供 API ID 以及资源 ID。这些 ID 可以在 API Gateway 控制台中轻松找到,如下所示:

对于像我这样的 CLI 爱好者,您也可以通过运行以下命令来获取这些信息:

    • REST API ID:
aws apigateway get-rest-apis --query "items[?name==\`MoviesAPI\`].id" --output text
    • 资源 ID:
aws apigateway get-resources --rest-api-id API_ID --query "items[?path==\`/movies\`].id" --output text
  1. 现在已经定义了 ID,更新aws apigateway put-method命令,使用你的 ID 并执行该命令。

  2. 接下来,将DeleteMovie函数设置为DELETE方法的目标:

aws apigateway put-integration \
 --rest-api-id API_ID \
 --resource-id RESOURCE_ID \
 --http-method DELETE \
 --type AWS_PROXY \
 --integration-http-method DELETE \
 --uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:ACCOUNT_ID:function:DeleteMovie/invocations \
 --region us-east-1
  1. 最后,告诉 API Gateway 跳过任何翻译,并在 Lambda 函数返回的响应中不做任何修改:
aws apigateway put-method-response \
 --rest-api-id API_ID \
 --resource-id RESOURCE_ID \
 --http-method DELETE \
 --status-code 200 \
 --response-models '{"application/json": "Empty"}' \
 --region us-east-1
  1. 在资源面板中,应该定义一个DELETE方法,如下所示:

  1. 使用以下 AWS CLI 命令重新部署 API:
aws apigateway create-deployment \
 --rest-api-id API_ID \
 --stage-name staging \
 --region us-east-1
  1. 使用以下 cURL 命令删除电影:
curl -sX DELETE -d '{"id":"1", "name":"Captain America"}' https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'
  1. 通过调用findAll端点的以下 cURL 命令来验证电影是否已被删除:
curl -sX GET https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies | jq '.'
  1. ID 为 1 的电影不会出现在返回的列表中。您可以在 DynamoDB 控制台中验证电影已成功删除,如下所示:

确实,ID 为 1 的电影不再存在于movies表中。

到目前为止,我们已经使用 AWS Lambda,API Gateway 和 DynamoDB 创建了一个无服务器 RESTful API。

摘要

在本章中,您学会了如何使用 Lambda 和 API Gateway 构建事件驱动的 API,以及如何在 DynamoDB 中存储数据。在后面的章节中,我们将进一步添加 API Gateway 顶部的安全层,构建 CI/CD 管道以自动化部署,等等。

在下一章中,我们将介绍一些高级的 AWS CLI 命令和选项,您可以在构建 AWS Lambda 中的无服务器函数时使用这些选项来节省时间。我们还将看到如何创建和维护多个版本和发布 Lambda 函数。

问题

  1. 实现一个update处理程序来更新现有的电影项目。

  2. 在 API Gateway 中创建一个新的 PUT 方法来触发update Lambda 函数。

  3. 实现一个单一的 Lambda 函数来处理所有类型的事件(GET,POST,DELETE,PUT)。

  4. 更新findOne处理程序以返回有效请求的正确响应代码,但是空数据(例如,请求的 ID 没有电影)。

  5. findAll端点上使用Range头和Query字符串实现分页系统。

第六章:部署您的无服务器应用程序

在之前的章节中,我们学习了如何从头开始构建一个无服务器 API。在本章中,我们将尝试完成以下内容:

  • 通过一些高级 AWS CLI 命令构建、部署和管理我们的 Lambda 函数

  • 发布 API 的多个版本

  • 学习如何使用别名分隔多个部署环境(沙盒、暂存和生产)

  • 覆盖 API Gateway 阶段变量的使用,以更改方法端点的行为。

Lambda CLI 命令

在本节中,我们将介绍各种 AWS Lambda 命令,您可能在构建 Lambda 函数时使用。我们还将学习如何使用它们来自动化您的部署过程。

列出函数命令

如果您还记得,此命令是在第二章中引入的,开始使用 AWS Lambda。顾名思义,它会列出您提供的 AWS 区域中的所有 Lambda 函数。以下命令将返回北弗吉尼亚地区的所有 Lambda 函数:

aws lambda list-functions --region us-east-1

对于每个函数,响应中都包括函数的配置信息(FunctionName、资源使用情况、Environment变量、IAM 角色、Runtime环境等),如下截图所示:

要仅列出一些属性,例如函数名称,可以使用query筛选选项,如下所示:

aws lambda list-functions --query Functions[].FunctionName[]

创建函数命令

如果您已经阅读了前面的章节,您应该对此命令很熟悉,因为它已经多次用于从头开始创建新的 Lambda 函数。

除了函数的配置,您还可以使用该命令以两种方式提供部署包(ZIP):

  • ZIP 文件:它使用--zip-file选项提供代码的 ZIP 文件路径:
aws lambda create-function --function-name UpdateMovie \
 --description "Update an existing movie" \
 --runtime go1.x \
 --role arn:aws:iam::ACCOUNT_ID:role/UpdateMovieRole \
 --handler main \
 --environment Variables={TABLE_NAME=movies} \
 --zip-file fileb://./deployment.zip \
 --region us-east-1a
  • S3 存储桶对象:它使用--code选项提供 S3 存储桶和对象名称:
aws lambda create-function --function-name UpdateMovie \
 --description "Update an existing movie" \
 --runtime go1.x \
 --role arn:aws:iam::ACCOUNT_ID:role/UpdateMovieRole \
 --handler main \
 --environment Variables={TABLE_NAME=movies} \
 --code S3Bucket=movies-api-deployment-package,S3Key=deployment.zip \
 --region us-east-1

如上述命令将以 JSON 格式返回函数设置的摘要,如下所示:

值得一提的是,在创建 Lambda 函数时,您可以根据函数的行为覆盖计算使用率和网络设置,使用以下选项:

  • --timeout:默认执行超时时间为三秒。当达到三秒时,AWS Lambda 终止您的函数。您可以设置的最大超时时间为五分钟。

  • --memory-size:执行函数时分配给函数的内存量。默认值为 128 MB,最大值为 3,008 MB(以 64 MB 递增)。

  • --vpc-config:这将在私有 VPC 中部署 Lambda 函数。虽然如果函数需要与内部资源通信,这可能很有用,但最好避免,因为它会影响 Lambda 的性能和扩展性(这将在即将到来的章节中讨论)。

AWS 不允许您设置函数的 CPU 使用率,因为它是根据为函数分配的内存自动计算的。 CPU 使用率与内存成比例。

更新函数代码命令

除了 AWS 管理控制台外,您还可以使用 AWS CLI 更新 Lambda 函数的代码。该命令需要目标 Lambda 函数名称和新的部署包。与上一个命令类似,您可以按以下方式提供包:

  • .zip文件的路径:
aws lambda update-function-code --function-name UpdateMovie \
    --zip-file fileb://./deployment-1.0.0.zip \
    --region us-east-1
  • 存储.zip文件的 S3 存储桶:
aws lambda update-function-code --function-name UpdateMovie \
    --s3-bucket movies-api-deployment-packages \
    --s3-key deployment-1.0.0.zip \
    --region us-east-1

此操作会为 Lambda 函数代码中的每个更改打印一个新的唯一 ID(称为RevisionId):

获取函数配置命令

为了检索 Lambda 函数的配置信息,请发出以下命令:

aws lambda get-function-configuration --function-name UpdateMovie --region us-east-1

前面的命令将以输出提供与使用create-function命令时显示的相同信息。

要检索特定 Lambda 版本或别名的配置信息(下一节),您可以使用--qualifier选项。

调用命令

到目前为止,我们直接从 AWS Lambda 控制台和通过 API Gateway 的 HTTP 事件调用了我们的 Lambda 函数。除此之外,Lambda 还可以通过 AWS CLI 使用invoke命令进行调用:

aws lambda invoke --function-name UpdateMovie result.json

上述命令将调用UpdateMovie函数,并将函数的输出保存在result.json文件中:

状态码为 400,这是正常的,因为UpdateFunction需要 JSON 输入。让我们看看如何使用invoke命令向我们的函数提供 JSON。

返回到 DynamoDB 的movies表,并选择要更新的电影。在本例中,我们将更新 ID 为 13 的电影,如下所示:

创建一个包含新电影项目属性的body属性的 JSON 文件,因为 Lambda 函数期望输入以 API Gateway 代理请求格式呈现:

{
  "body": "{\"id\":\"13\", \"name\":\"Deadpool 2\"}"
}

最后,再次运行invoke函数命令,将 JSON 文件作为输入参数:

aws lambda invoke --function UpdateMovie --payload file://input.json result.json

如果打印result.json的内容,更新后的电影应该会返回,如下所示:

您可以通过调用FindAllMovies函数来验证 DynamoDB 表中电影的名称是否已更新:

aws lambda invoke --function-name FindAllMovies result.json

body属性应该包含新更新的电影,如下所示:

返回到 DynamoDB 控制台;ID 为 13 的电影应该有一个新的名称,如下截图所示:

删除函数命令

要删除 Lambda 函数,您可以使用以下命令:

aws lambda delete-function --function-name UpdateMovie

默认情况下,该命令将删除所有函数版本和别名。要删除特定版本或别名,您可能需要使用--qualifier选项。

到目前为止,您应该熟悉了在构建 AWS Lambda 中的无服务器应用程序时可能使用和需要的所有 AWS CLI 命令。在接下来的部分中,我们将看到如何创建 Lambda 函数的不同版本,并使用别名维护多个环境。

版本和别名

在构建无服务器应用程序时,您必须将部署环境分开,以便在不影响生产的情况下测试新更改。因此,拥有多个 Lambda 函数版本是有意义的。

版本控制

版本代表了您函数代码和配置在某个时间点的状态。默认情况下,每个 Lambda 函数都有一个$LATEST版本,指向您函数的最新更改,如下截图所示:

要从$LATEST版本创建新版本,请单击“操作”并选择“发布新版本”。让我们称其为1.0.0,如下截图所示:

新版本将创建一个 ID=1(递增)。请注意以下截图中窗口顶部的 ARN Lambda 函数;它具有版本 ID:

版本创建后,您无法更新函数代码,如下所示:

此外,高级设置,如 IAM 角色、网络配置和计算使用情况,无法更改,如下所示:

版本被称为不可变,这意味着一旦发布,它们就无法更改;只有$LATEST版本是可编辑的。

现在,我们知道如何从控制台发布新版本。让我们使用 AWS CLI 发布一个新版本。但首先,我们需要更新FindAllMovies函数,因为如果自从发布版本1.0.0以来对$LATEST没有进行任何更改,我们就无法发布新版本。

新版本将具有分页系统。该函数将仅返回用户请求的项目数量。以下代码将读取Count头参数,将其转换为数字,并使用带有Limit参数的Scan操作从 DynamoDB 中获取电影:

func findAll(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  size, err := strconv.Atoi(request.Headers["Count"])
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusBadRequest,
      Body: "Count Header should be a number",
    }, nil
  }

  ...

  svc := dynamodb.New(cfg)
  req := svc.ScanRequest(&dynamodb.ScanInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
    Limit: aws.Int64(int64(size)),
  })

  ...
}

接下来,使用update-function-code命令更新FindAllMovies Lambda 函数的代码:

aws lambda update-function-code --function-name FindAllMovies \
    --zip-file fileb://./deployment.zip

然后,基于当前配置和代码,使用以下命令发布一个新版本1.1.0

aws lambda publish-version --function-name FindAllMovies --description 1.1.0

返回到 AWS Lambda 控制台,导航到您的FindAllMovies;应该创建一个新版本,ID=2,如下截图所示:

现在我们的版本已经创建,让我们通过使用 AWS CLI invoke命令来测试它们。

FindAllMovies v1.0.0

使用以下命令在限定参数中调用FindAllMovies v1.0.0 版本:

aws lambda invoke --function-name FindAllMovies --qualifier 1 result.json

result.json应该包含 DynamoDBmovies表中的所有电影,如下所示:

输出显示 DynamoDB 电影表中的所有电影

FindAllMovies v1.1.0

创建一个名为input.json的新文件,并粘贴以下内容。此函数的版本需要一个名为Count的 Header 参数,用于返回电影的数量:

{
  "headers": {
    "Count": "4"
  }
}

执行该函数,但这次使用--payload参数和指向input.json文件的路径位置:

aws lambda invoke --function-name FindAllMovies --payload file://input.json
    --qualifier 2 result.json

result.json应该只包含四部电影,如下所示:

这就是如何创建多个版本的 Lambda 函数。但是,Lambda 函数版本控制的最佳实践是什么?

语义化版本控制

当您发布 Lambda 函数的新版本时,应该给它一个重要且有意义的版本名称,以便您可以通过其开发周期跟踪对函数所做的不同更改。

当您构建一个将被数百万客户使用的公共无服务器 API 时,您命名不同 API 版本的方式至关重要,因为它允许您的客户知道新版本是否引入了破坏性更改。它还让他们选择合适的时间升级到最新版本,而不会冒太多破坏他们流水线的风险。

这就是语义化版本控制(semver.org)的作用,它是一种使用三个数字序列的版本方案:

每个数字都根据以下规则递增:

  • 主要:如果 Lambda 函数与先前版本不兼容,则递增。

  • 次要:如果新功能或特性已添加到函数中,并且仍然向后兼容,则递增。

  • 补丁:如果修复了错误和问题,并且函数仍然向后兼容,则递增。

例如,FindAllMovies函数的版本1.1.0是第一个主要版本,带有一个次要版本带来了一个新功能(分页系统)。

别名

别名是指向特定版本的指针,它允许您将函数从一个环境提升到另一个环境(例如从暂存到生产)。别名是可变的,而版本是不可变的。

为了说明别名的概念,我们将创建两个别名,如下图所示:一个指向FindAllMovies Lambda 函数1.0.0版本的Production别名,和一个指向函数1.1.0版本的Staging别名。然后,我们将配置 API Gateway 使用这些别名,而不是$LATEST版本:

返回到FindAllMovies配置页面。如果单击Qualifiers下拉列表,您应该看到一个名为Unqualified的默认别名,指向您的$LATEST版本,如下截图所示:

要创建一个新别名,单击操作,然后创建一个名为Staging的新别名。选择5版本作为目标,如下所示:

创建后,新版本应添加到别名列表中,如下所示:

接下来,使用 AWS 命令行为Production环境创建一个指向版本1.0.0的新别名:

aws lambda create-alias --function-name FindAllMovies \
    --name Production --description "Production environment" \
    --function-version 1

同样,新别名应成功创建:

现在我们已经创建了别名,让我们配置 API Gateway 以使用这些别名和阶段变量

阶段变量

阶段变量是环境变量,可用于在每个部署阶段运行时更改 API Gateway 方法的行为。接下来的部分将说明如何在 API Gateway 中使用阶段变量。

在 API Gateway 控制台上,导航到Movies API,单击GET方法,并更新目标 Lambda 函数,以使用阶段变量而不是硬编码的 Lambda 函数名称,如下截图所示:

当您保存时,将会出现一个新的提示,要求您授予 API Gateway 调用 Lambda 函数别名的权限,如下截图所示:

执行以下命令以允许 API Gateway 调用ProductionStaging别名:

  • Production 别名:
aws lambda add-permission --function-name "arn:aws:lambda:us-east-1:ACCOUNT_ID:function:FindAllMovies:Production" \
 --source-arn "arn:aws:execute-api:us-east-1:ACCOUNT_ID:API_ID/*/GET/movies" \
 --principal apigateway.amazonaws.com \
 --statement-id STATEMENT_ID \
 --action lambda:InvokeFunction
  • Staging 别名:
aws lambda add-permission --function-name "arn:aws:lambda:us-east-1:ACCOUNT_ID:function:FindAllMovies:Staging" \
 --source-arn "arn:aws:execute-api:us-east-1:ACCOUNT_ID:API_ID/*/GET/movies" \
 --principal apigateway.amazonaws.com \
 --statement-id STATEMENT_ID \
 --action lambda:InvokeFunction

然后,创建一个名为production的新阶段,如下截图所示:

接下来,单击Stages Variables选项卡,并创建一个名为lambda的新阶段变量,并将FindAllMovies:Production设置为值,如下所示:

对于staging环境,使用指向 Lambda 函数Staging别名的lambda变量进行相同操作,如下所示:

要测试端点,请使用cURL命令或您熟悉的任何 REST 客户端。我选择了 Postman。在 API Gateway 的production阶段调用的 URL 上使用GET方法应该返回数据库中的所有电影,如下所示:

对于staging环境,执行相同操作,使用名为Count=4的新Header键;您应该只返回四个电影项目,如下所示:

这就是您可以维护 Lambda 函数的多个环境的方法。现在,您可以通过将Production指针从1.0.0更改为1.1.0而轻松将1.1.0版本推广到生产环境,并在失败时回滚到以前的工作版本,而无需更改 API Gateway 设置。

摘要

AWS CLI 对于创建自动化脚本来管理 AWS Lambda 函数非常有用。

版本是不可变的,一旦发布就无法更改。另一方面,别名是动态的,它们的绑定可以随时更改以实现代码推广或回滚。采用 Lambda 函数版本的语义化版本控制可以更容易地跟踪更改。

在下一章中,我们将学习如何从头开始设置 CI/CD 流水线,以自动化部署 Lambda 函数到生产环境的过程。我们还将介绍如何在持续集成工作流程中使用别名和版本。

第七章:实施 CI/CD 流水线

本章将讨论高级概念,如:

  • 如何建立一个高度弹性和容错的 CI/CD 流水线,自动化部署您的无服务器应用程序

  • 拥有一个用于 Lambda 函数的集中式代码存储库的重要性

  • 如何自动部署代码更改到生产环境。

技术要求

在开始本章之前,请确保您已经创建并上传了之前章节中构建的函数的源代码到一个集中的 GitHub 存储库。此外,强烈建议具有 CI/CD 概念的先前经验。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go

持续集成和部署工作流

持续集成、持续部署和持续交付是加速软件上市时间并通过反馈推动创新的绝佳方式,同时确保在每次迭代中构建高质量产品。但这些实践意味着什么?在构建 AWS Lambda 中的无服务器应用程序时,如何应用这些实践?

持续集成

持续集成CI)是指拥有一个集中的代码存储库,并在将所有更改和功能整合到中央存储库之前,通过一个复杂的流水线进行处理的过程。经典的 CI 流水线在代码提交时触发构建,运行单元测试和所有预整合测试,构建构件,并将结果推送到构件管理存储库。

持续部署

持续部署CD)是持续集成的延伸。通过持续集成流水线的所有阶段的每个更改都会自动发布到您的暂存环境。

持续交付

持续交付CD)与 CD 类似,但在将发布部署到生产环境之前需要人工干预或业务决策。

现在这些实践已经定义,您可以使用这些概念来利用自动化的力量,并构建一个端到端的部署流程,如下图所示:

在接下来的章节中,我们将介绍如何使用最常用的 CI 解决方案构建这个流水线。

为了说明这些概念,只使用FindAllMovies函数的代码,但相同的步骤可以应用于其他 Lambda 函数。

自动化部署 Lambda 函数

在本节中,我们将看到如何构建一个流水线,以不同的方式自动化部署前一章中构建的 Lambda 函数的部署过程。

  • 由 AWS 管理的解决方案,如 CodePipeline 和 CodeBuild

  • 本地解决方案,如 Jenkins

  • SaaS 解决方案,如 Circle CI

使用 CodePipeline 和 CodeBuild 进行持续部署

AWS CodePipeline 是一个工作流管理工具,允许您自动化软件的发布和部署过程。用户定义一组步骤,形成一个可以在 AWS 托管服务(如 CodeBuild 和 CodeDeploy)或第三方工具(如 Jenkins)上执行的 CI 工作流。

在本例中,AWS CodeBuild 将用于测试、构建和部署您的 Lambda 函数。因此,应在代码存储库中创建一个名为buildspec.yml的构建规范文件。

buildspec.yml定义了将在 CI 服务器上执行的一组步骤,如下所示:

version: 0.2
env:
 variables:
 S3_BUCKET: "movies-api-deployment-packages"
 PACKAGE: "github.com/mlabouardy/lambda-codepipeline"

phases:
 install:
 commands:
 - mkdir -p "/go/src/$(dirname ${PACKAGE})"
 - ln -s "${CODEBUILD_SRC_DIR}" "/go/src/${PACKAGE}"
 - go get -u github.com/golang/lint/golint

 pre_build:
 commands:
 - cd "/go/src/${PACKAGE}"
 - go get -t ./...
 - golint -set_exit_status
 - go vet .
 - go test .

 build:
 commands:
 - GOOS=linux go build -o main
 - zip $CODEBUILD_RESOLVED_SOURCE_VERSION.zip main
 - aws s3 cp $CODEBUILD_RESOLVED_SOURCE_VERSION.zip s3://$S3_BUCKET/

 post_build:
 commands:
 - aws lambda update-function-code --function-name FindAllMovies --s3-bucket $S3_BUCKET --s3-key $CODEBUILD_RESOLVED_SOURCE_VERSION.zip

构建规范分为以下四个阶段:

  • 安装

  • 设置 Go 工作空间

  • 安装 Go linter

  • 预构建:

  • 安装 Go 依赖项

  • 检查我们的代码是否格式良好,并遵循 Go 的最佳实践和常见约定

  • 使用go test命令运行单元测试

  • 构建

  • 使用go build命令构建单个二进制文件

  • 从生成的二进制文件创建一个部署包.zip

  • .zip文件存储在 S3 存储桶中

  • 后构建

  • 使用新的部署包更新 Lambda 函数的代码

单元测试命令将返回一个空响应,因为我们将在即将到来的章节中编写我们的 Lambda 函数的单元测试。

源提供者

现在我们的工作流已经定义,让我们创建一个持续部署流水线。打开 AWS 管理控制台(console.aws.amazon.com/console/home),从开发人员工具部分导航到 AWS CodePipeline,并创建一个名为 MoviesAPI 的新流水线,如下图所示:

在源位置页面上,选择 GitHub 作为源提供者,如下图所示:

除了 GitHub,AWS CodePipeline 还支持 Amazon S3 和 AWS CodeCommit 作为代码源提供者。

点击“连接到 GitHub”按钮,并授权 CodePipeline 访问您的 GitHub 存储库;然后,选择存储代码的 GitHub 存储库和要构建的目标 git 分支,如下图所示:

构建提供者

在构建阶段,选择 AWS CodeBuild 作为构建服务器。Jenkins 和 Solano CI 也是支持的构建提供者。请注意以下截图:

在创建流水线的下一步是定义一个新的 CodeBuild 项目,如下所示:

将构建服务器设置为带有 Golang 的 Ubuntu 实例作为运行时环境,如下图所示:

构建环境也可以基于 DockerHub 上公开可用的 Docker 镜像或私有注册表,例如弹性容器注册表ECR)。

CodeBuild 将在 S3 存储桶中存储构件(部署包),并更新 Lambda 函数的FindAllMovies代码。因此,应该附加一个具有以下策略的 IAM 角色:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "lambda:UpdateFunctionCode"
      ],
      "Resource": [
        "arn:aws:s3:::movies-api-deployment-packages/*",
        "arn:aws:lambda:us-east-1:305929695733:function:FindAllMovies"
      ]
    }
  ]
}

在上述代码块中,arn:aws:lambda:us-east-1帐户 ID 应该替换为您的帐户 ID。

部署提供者

项目构建完成后,在流水线中配置的下一步是部署到一个环境。在本章中,我们将选择无部署选项,并让 CodeBuild 使用 AWS CLI 将新代码部署到 Lambda,如下图所示:

这个部署过程需要解释无服务器应用程序模型和 CloudFormation,这将在后续章节中详细解释。

审查详细信息;当您准备好时,点击保存,将创建一个新的流水线,如下所示:

流水线将启动,并且构建阶段将失败,如下图所示:

如果我们点击“详细信息”链接,它将带您到该特定构建的 CodeBuild 项目页面。可以在这里看到描述构建规范文件的阶段:

如图所示,预构建阶段失败了;在底部的日志部分,我们可以看到这是由于golint命令:

在 Golang 中,所有顶级的、公开的名称(大写)都应该有文档注释。因此,应该在 Movie 结构声明的顶部添加一个新的注释,如下所示:

// Movie entity
type Movie struct {
  ID string `json:"id"`
  Name string `json:"name"`
}

将新更改提交到 GitHub,新的构建将触发流水线的执行:

您可能想知道如何将代码更改推送到代码存储库会触发新的构建。答案是 GitHub Webhooks。当您创建 CodeBuild 项目时,GitHub 存储库中会自动创建一个新的 Webhook。因此,所有对代码存储库的更改都会通过 CI 流水线,如下截图所示:

一旦流水线完成,所有 CodeBuild 阶段都应该通过,如下截图所示:

打开 S3 控制台,然后单击流水线使用的存储桶;新的部署包应该以与提交 ID 相同的键名存储:

最后,CodeBuild 将使用update-function-code命令更新 Lambda 函数的代码。

使用 Jenkins 的连续管道

多年来,Jenkins 一直是首选工具。它是一个用 Java 编写的开源持续集成服务器,构建在 Hudson 项目之上。由于其插件驱动的架构和丰富的生态系统,它具有很高的可扩展性。

在接下来的部分中,我们将使用 Jenkins 编写我们的第一个Pipeline as Code,但首先我们需要设置我们的 Jenkins 环境。

分布式构建

要开始,请按照此指南中的官方说明安装 Jenkins:jenkins.io/doc/book/installing/。一旦 Jenkins 启动并运行,将浏览器指向http://instance_ip:8080。此链接将打开 Jenkins 仪表板,如下截图所示:

使用 Jenkins 的一个优势是其主/从架构。它允许您设置一个 Jenkins 集群,其中有多个负责构建应用程序的工作节点(代理)。这种架构有许多好处:

  • 响应时间,队列中等待构建的作业不多

  • 并发构建数量增加

  • 支持多个平台

以下步骤描述了为 Jenkins 构建服务器启动新工作节点的配置过程。工作节点是一个 EC2 实例,安装了最新稳定版本的JDK8Golang(有关说明,请参见第二章,使用 AWS Lambda 入门)。

工作节点运行后,将其 IP 地址复制到剪贴板,返回 Jenkins 主控台,单击“管理 Jenkins”,然后单击“管理节点”。单击“新建节点”,给工作节点命名,并选择永久代理,如下截图所示:

然后,将节点根目录设置为 Go 工作空间,并粘贴节点的 IP 地址并选择 SSH 密钥,如下所示:

如果一切配置正确,节点将上线,如下所示:

设置 Jenkins 作业

现在我们的集群已部署,我们可以编写我们的第一个 Jenkins 流水线。这个流水线定义在一个名为Jenkinsfile的文本文件中。这个定义文件必须提交到 Lambda 函数的代码存储库中。

Jenkins 必须安装Pipeline插件才能使用Pipeline as Code功能。这个功能提供了许多即时的好处,比如代码审查、回滚和版本控制。

考虑以下Jenkinsfile,它实现了一个基本的五阶段连续交付流水线,用于FindAllMovies Lambda 函数:

def bucket = 'movies-api-deployment-packages'

node('slave-golang'){
    stage('Checkout'){
        checkout scm
    }

    stage('Test'){
        sh 'go get -u github.com/golang/lint/golint'
        sh 'go get -t ./...'
        sh 'golint -set_exit_status'
        sh 'go vet .'
        sh 'go test .'
    }

    stage('Build'){
        sh 'GOOS=linux go build -o main main.go'
        sh "zip ${commitID()}.zip main"
    }

    stage('Push'){
        sh "aws s3 cp ${commitID()}.zip s3://${bucket}"
    }

    stage('Deploy'){
        sh "aws lambda update-function-code --function-name FindAllMovies \
                --s3-bucket ${bucket} \
                --s3-key ${commitID()}.zip \
                --region us-east-1"
    }
}

def commitID() {
    sh 'git rev-parse HEAD > .git/commitID'
    def commitID = readFile('.git/commitID').trim()
    sh 'rm .git/commitID'
    commitID
}

流水线使用基于 Groovy 语法的领域特定语言DSL)编写,并将在我们之前添加到集群的节点上执行。每次对 GitHub 存储库进行更改时,您的更改都将经过多个阶段:

  • 检查来自源代码控制的代码

  • 运行单元和质量测试

  • 构建部署包并将此构件存储到 S3 存储桶

  • 更新FindAllMovies函数的代码

请注意使用 git 提交 ID 作为部署包的名称,以便为每个发布提供有意义且重要的名称,并且如果出现问题,可以回滚到特定的提交。

现在我们的管道已经定义好,我们需要通过单击“新建”在 Jenkins 上创建一个新作业。然后,为作业输入名称,并选择多分支管道。设置存储您的 Lambda 函数代码的 GitHub 存储库以及Jenkinsfile的路径如下:

在构建之前,必须在 Jenkins 工作程序上配置具有对 S3 的写访问权限和对 Lambda 的更新操作的 IAM 实例角色。

保存后,管道将在主分支上执行,并且作业应该变为绿色,如下所示:

管道完成后,您可以单击每个阶段以查看执行日志。在以下示例中,我们可以看到部署阶段的日志:

Git 钩子

最后,为了使 Jenkins 在您推送到代码存储库时触发构建,请从您的 GitHub 存储库中单击设置,然后在集成和服务中搜索Jenkins(GitHub 插件),并填写类似以下的 URL:

现在,每当您将代码推送到 GitHub 存储库时,完整的 Jenkins 管道将被触发,如下所示:

另一种使 Jenkins 在检测到更改时创建构建的方法是定期轮询目标 git 存储库(cron 作业)。这种解决方案效率有点低,但如果您的 Jenkins 实例在私有网络中,这可能是有用的。

使用 Circle CI 进行持续集成

CircleCI 是“CI/CD 即服务”。这是一个与基于 GitHub 和 BitBucket 的项目非常好地集成,并且内置支持 Golang 应用程序的平台。

在接下来的部分中,我们将看到如何使用 CircleCI 自动化部署我们的 Lambda 函数的过程。

身份和访问管理

使用您的 GitHub 帐户登录 Circle CI(circleci.com/vcs-authorize/)。然后,选择存储您的 Lambda 函数代码的存储库,然后单击“设置项目”按钮,以便 Circle CI 可以自动推断设置,如下面的屏幕截图所示:

与 Jenkins 和 CodeBuild 类似,CircleCI 将需要访问一些 AWS 服务。因此,需要一个 IAM 用户。返回 AWS 管理控制台,并创建一个名为circleci的新 IAM 用户。生成 AWS 凭据,然后从 CircleCI 项目的设置中点击“设置”,然后粘贴 AWS 访问和秘密密钥,如下面的屏幕截图所示:

确保附加了具有对 S3 读/写权限和 Lambda 函数的权限的 IAM 策略到 IAM 用户。

配置 CI 管道

现在我们的项目已经设置好,我们需要定义 CI 工作流程;为此,我们需要在.circleci文件夹中创建一个名为config.yml的定义文件,其中包含以下内容:

version: 2
jobs:
  build:
    docker:
      - image: golang:1.8

    working_directory: /go/src/github.com/mlabouardy/lambda-circleci

    environment:
        S3_BUCKET: movies-api-deployment-packages

    steps:
      - checkout

      - run:
         name: Install AWS CLI & Zip
         command: |
          apt-get update
          apt-get install -y zip python-pip python-dev
          pip install awscli

      - run:
          name: Test
          command: |
           go get -u github.com/golang/lint/golint
           go get -t ./...
           golint -set_exit_status
           go vet .
           go test .

      - run:
         name: Build
         command: |
          GOOS=linux go build -o main main.go
          zip $CIRCLE_SHA1.zip main

      - run:
          name: Push
          command: aws s3 cp $CIRCLE_SHA1.zip s3://$S3_BUCKET

      - run:
          name: Deploy
          command: |
            aws lambda update-function-code --function-name FindAllMovies \
                --s3-bucket $S3_BUCKET \
                --s3-key $CIRCLE_SHA1.zip --region us-east-1

构建环境将是 DockerHub 中 Go 官方 Docker 镜像。从该镜像中,将创建一个新容器,并按照steps部分中列出的命令执行:

  1. 从 GitHub 存储库中检出代码。

  2. 安装 AWS CLI 和 ZIP 命令。

  3. 执行自动化测试。

  4. 从源代码构建单个二进制文件并压缩部署包。与构建对应的提交 ID 将用作 zip 文件的名称(请注意使用CIRCLE_SHA1环境变量)。

  5. 将工件保存在 S3 存储桶中。

  6. 使用 AWS CLI 更新 Lambda 函数的代码。

一旦模板被定义并提交到 GitHub 存储库,将触发新的构建,如下所示:

当流水线成功运行时,它会是这个样子:

基本上就是这样。本章只是初步介绍了 CI/CD 流水线的功能,但应该为您开始实验和构建 Lambda 函数的端到端工作流提供了足够的基础。

总结

在本章中,我们学习了如何从头开始设置 CI/CD 流水线,自动化 Lambda 函数的部署过程,以及如何使用不同的 CI 工具和服务来实现这个解决方案,从 AWS 托管服务到高度可扩展的构建服务器。

在下一章中,我们将通过为我们的无服务器 API 编写自动化单元和集成测试,以及使用无服务器函数构建带有 REST 后端的单页面应用程序,构建这个流水线的改进版本。

问题

  1. 使用 CodeBuild 和 CodePipeline 为其他 Lambda 函数实现 CI/CD 流水线。

  2. 使用 Jenkins Pipeline 实现类似的工作流。

  3. 使用 CircleCI 实现相同的流水线。

  4. 在现有流水线中添加一个新阶段,如果当前的 git 分支是主分支,则发布一个新版本。

  5. 配置流水线,在每次部署或更新新的 Lambda 函数时,在 Slack 频道上发送通知。

第八章:扩展您的应用程序

本章是上一技术章节的一个短暂休息,我们将深入探讨以下内容:

  • 无服务器自动扩展的工作原理

  • Lambda 如何在高峰服务使用期间处理流量需求,而无需容量规划或定期扩展

  • AWS Lambda 如何使用并发性来并行创建多个执行以执行函数的代码

  • 它如何影响您的成本和应用程序性能。

技术要求

本章是上一章的后续,因为它将使用上一章中构建的无服务器 API;建议在处理本节之前先阅读上一章。

负载测试和扩展

在这部分中,我们将生成随机工作负载,以查看 Lambda 在传入请求增加时的表现。为了实现这一点,我们将使用负载测试工具,比如Apache Bench。在本章中,我将使用hey,这是一个基于 Go 的工具,由于 Golang 内置的并发性,它非常高效和快速,比传统的HTTP基准测试工具更快。您可以通过在终端中安装以下Go包来下载它:

go get -u github.com/rakyll/hey

确保$GOPATH变量被设置,以便能够在任何当前目录下执行hey命令,或者您可以将$HOME/go/bin文件夹添加到$PATH变量中。

Lambda 自动扩展

现在,我们准备通过执行以下命令来运行我们的第一个测试或负载测试:

hey -n 1000 -c 50 https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies

如果您更喜欢 Apache Benchmark,可以通过将hey关键字替换为ab来使用相同的命令。

该命令将打开 50 个连接,并对 API Gateway 端点 URL 执行 1,000 个请求,用于FindAllMovies函数。在测试结束时,hey将显示有关总响应时间的信息以及每个请求的详细信息,如下所示:

确保用您自己的调用 URL 替换调用 URL。另外,请注意,截图的某些部分已被裁剪,以便只关注有用的内容。

除了总响应时间外,hey还输出了一个响应时间直方图,显示第一个请求花费更多时间(大约 2 秒)来响应,这可以解释为 Lambda 需要下载部署包并初始化新容器的冷启动。然而,其余的请求很快(不到 800 毫秒),这是由于热启动和使用先前请求的现有容器。

从先前的基准测试中,我们可以说 Lambda 在流量增加时保持了自动扩展的承诺;虽然这可能是一件好事,但它也有缺点,我们将在下一节中看到。

下游资源

在我们的 Movies API 示例中,DynamoDB 表已被用于解决无状态问题。该表要求用户提前定义读取和写入吞吐量容量,以创建必要的基础设施来处理定义的流量。在第五章中,使用 DynamoDB 管理数据持久性,我们使用了默认的吞吐量,即五个读取容量单位和五个写入容量单位。五个读取容量单位对于不太重读的 API 来说非常有效。在先前的负载测试中,我们创建了 50 个并发执行,也就是说,对movies表进行了 50 次并行读取。结果,表将遭受高读取吞吐量,并且Scan操作将变慢,DynamoDB 可能会开始限制请求。

我们可以通过转到 DynamoDB 控制台,并点击movies表的Metrics选项卡来验证这一点,如下截图所示:

显然,读取容量图经历了一个高峰期,导致读取请求被限制,并且表格被所有这些传入的请求压倒。

DynamoDB 的限流请求可以通过启用自动扩展机制来增加预留的读写容量以处理突然增加的流量,或者通过重用存储在内存缓存引擎中的查询结果(可以使用 AWS ElastiCache 与 Redis 或 Memcached 引擎等解决方案)来避免过载表并减少函数执行时间。但是,您无法限制和保护数据库资源免受 Lambda 函数扩展事件的影响。

私有 Lambda 函数

如果您的 Lambda 函数在私有 VPC 中运行,可能会出现并发问题,因为它需要将弹性网络接口(ENI)附加到 Lambda 容器,并等待分配 IP 地址。AWS Lambda 使用 ENI 安全连接到 VPC 中的内部资源。

除了性能不佳(附加 ENI 平均需要 4 秒),启用 VPC 的 Lambda 函数还需要您维护和配置一个用于互联网访问的 NAT 实例和多个可支持函数 ENI 扩展需求的 VPC 子网,这可能导致 VPC 的 IP 地址用尽。

总之,Lambda 函数的自动扩展是一把双刃剑;它不需要您进行容量规划。但是,它可能导致性能不佳和令人惊讶的月度账单。这就是并发执行模型发挥作用的地方。

并发执行

AWS Lambda 会根据流量增加动态扩展容量。但是,每次执行函数的代码数量是有限的。这个数量称为并发执行,它是根据 AWS 区域定义的。默认并发限制是每个 AWS 区域 1000 个。那么,如果您的函数超过了这个定义的阈值会发生什么呢?继续阅读以了解详情。

Lambda 限流

如果并发执行计数超过限制,Lambda 会对您的函数应用限流(速率限制)。因此,剩余的传入请求将不会调用该函数。

调用客户端负责根据返回的HTTP代码(429 =请求过多)实施基于退避策略的重试失败请求。值得一提的是,Lambda 函数可以配置为在一定数量的重试后将未处理的事件存储到名为死信队列的队列中。

在某些情况下,限流可能是有用的,因为并发执行容量是所有函数共享的(在我们的示例中,findupdateinsertdelete函数)。您可能希望确保一个函数不会消耗所有容量,并避免其他 Lambda 函数的饥饿。如果您的某个函数比其他函数更常用,这种情况可能经常发生。例如,考虑FindAllMovies函数。假设现在是假期季,很多客户会使用您的应用程序查看可租用的电影列表,这可能导致多次调用FindAllMovies Lambda 函数。

幸运的是,AWS 增加了一个新功能,允许您预先保留和定义每个 Lambda 函数的并发执行值。这个属性允许您为函数指定一定数量的保留并发,以确保您的函数始终具有足够的容量来处理即将到来的事件或请求。例如,您可以为您的函数设置如下速率限制:

  • FindAllMovies函数:500

  • InsertMovie函数:100

  • UpdateMovie函数:50

  • 剩下的将分享给其他人

在接下来的部分中,我们将看到如何为FindAllMovies定义保留的并发执行,并且它如何影响 API 的性能。

您可以使用以下公式估算并发执行计数:每秒事件/请求*函数持续时间

并发执行预留

导航到 AWS Lambda 控制台(console.aws.amazon.com/lambda/home)并单击 FindAllMovies 函数。在并发 部分,我们可以看到我们的函数仅受账户中可用并发总量的限制,该总量为1000,如下截图所示:

我们将通过在保留账户的并发字段中定义 10 来更改这一点。这样可以确保在任何给定时间内只有 10 个并行执行函数。这个值将从未保留账户的并发池中扣除,如下所示:

您可以设置的最大保留并发数是 900,因为 AWS Lambda 保留了 100 个用于其他函数,以便它们仍然可以处理请求和事件。

或者,可以使用 AWS CLI 与put-function-concurrency命令来设置并发限制:

aws lambda put-function-concurrency --function FindAllMovies --reserved-concurrent-executions 10

再次使用之前给出的相同命令生成一些工作负载:

hey -n 1000 -c 50 https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies

这一次,结果将会不同,因为 1000 个请求中有 171 个失败,显示为 502 错误代码,如下所示:

超过 10 个并发执行时,将应用限流,并拒绝部分请求,返回 502 响应代码。

我们可以通过返回到函数控制台来确认这一点;我们应该看到类似于以下截图中显示的警告消息:

如果您打开与movies表相关的指标并跳转到读取容量图表,您会看到我们的读取容量仍然受到控制,并且低于定义的 5 个读取单位容量:

如果您计划对 Lambda 函数进行维护并希望暂时停止其调用,可以使用限流。这可以通过将函数并发设置为 0 来实现。

限流按预期工作,现在您正在保护下游资源免受 Lambda 函数过载的影响。

摘要

在本章中,我们了解到 Lambda 由于 AWS 区域设置的执行限制,无法无限扩展。这个限制可以通过联系 AWS 支持团队来提高。我们还介绍了函数级别的并发预留如何帮助您保护下游资源,如果您正在使用启用了 VPC 的 Lambda 函数,则匹配子网大小,并在开发和测试函数期间控制成本。

在下一章中,我们将在无服务器 API 的基础上构建一个用户友好的 UI,具有 S3 静态托管网站功能。

第九章:使用 S3 构建前端

在这一章中,我们将学习以下内容:

  • 如何构建一个消耗 API Gateway 响应的静态网站,使用 AWS 简单存储服务

  • 如何通过 CloudFront 分发优化对网站资产的访问,例如 JavaScript、CSS、图像

  • 如何为无服务器应用程序设置自定义域名

  • 如何创建 SSL 证书以使用 HTTPS 显示您的内容

  • 使用 CI/CD 管道自动化 Web 应用程序的部署过程。

技术要求

在继续本章之前,您应该对 Web 开发有基本的了解,并了解 DNS 的工作原理。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go

单页应用程序

在本节中,我们将学习如何构建一个 Web 应用程序,该应用程序将调用我们在之前章节中构建的 API Gateway 调用 URL,并列出电影,如下所示:

对于每部电影,我们将显示其封面图像和标题。此外,用户可以通过单击右侧的按钮来按类别筛选电影。最后,如果用户单击导航栏上的“New”按钮,将弹出一个模态框,要求用户填写以下字段:

现在应用程序的模拟已经定义,我们将使用 JavaScript 框架快速构建 Web 应用程序。例如,我将使用当前最新稳定版本的 Angular 5。

使用 Angular 开发 Web 应用程序

Angular 是由 Google 开发的完全集成的框架。它允许您构建动态 Web 应用程序,而无需考虑选择哪些库以及如何处理日常问题。请记住,目标是要吸引大量观众,因此选择了 Angular 作为最常用的框架之一。但是,您可以选择您熟悉的任何框架,例如 React、Vue 或 Ember。

除了内置的可用模块外,Angular 还利用了单页应用程序(SPA)架构的强大功能。这种架构允许您在不刷新浏览器的情况下在页面之间导航,因此使应用程序更加流畅和响应,包括更好的性能(您可以预加载和缓存额外的页面)。

Angular 自带 CLI。您可以按照cli.angular.io上的逐步指南进行安装。本书专注于 Lambda。因此,本章仅涵盖了 Angular 的基本概念,以便让那些不是 Web 开发人员的人能够轻松理解。

一旦安装了Angular CLI,我们需要使用以下命令创建一个新的 Angular 应用程序:

ng new frontend

CLI 将生成基本模板文件并安装所有必需的npm依赖项以运行 Angular 5 应用程序。文件结构如下:

接下来,在frontend目录中,使用以下命令启动本地 Web 服务器:

ng serve

该命令将编译所有的TypeScripts文件,构建项目,并在端口4200上启动 Web 服务器:

打开浏览器并导航至localhost:4200。您在浏览器中应该看到以下内容:

现在我们的示例应用程序已构建并运行,让我们创建我们的 Web 应用程序。Angular 结构基于组件和服务架构(类似于模型-视图-控制器)。

生成您的第一个 Angular 组件

对于那些没有太多 Angular 经验的人来说,组件基本上就是 UI 的乐高积木。您的 Web 应用程序可以分为多个组件。每个组件都有以下文件:

  • COMPONENT_NAME.component.ts:用 TypeScript 编写的组件逻辑定义

  • COMPONENT_NAME.component.html:组件的 HTML 代码

  • COMPONENT_NAME.component.css:组件的 CSS 结构

  • COMPONENT_NAME.component.spec.ts:组件类的单元测试

在我们的示例中,我们至少需要三个组件:

  • 导航栏组件

  • 电影列表组件

  • 电影组件

在创建第一个组件之前,让我们安装Bootstrap,这是 Twitter 开发的用于构建吸引人用户界面的前端 Web 框架。它带有一组基于 CSS 的设计模板,用于表单、按钮、导航和其他界面组件,以及可选的 JavaScript 扩展。

继续在终端中安装 Bootstrap 4:

npm install bootstrap@4.0.0-alpha.6

接下来,在.angular-cli.json文件中导入 Bootstrap CSS 类,以便在应用程序的所有组件中使 CSS 指令可用:

"styles": [
   "styles.css",
   "../node_modules/bootstrap/dist/css/bootstrap.min.css"
]

现在我们准备通过发出以下命令来创建我们的导航栏组件:

ng generate component components/navbar

覆盖默认生成的navbar.component.html中的 HTML 代码,以使用 Bootstrap 框架提供的导航栏:

<nav class="navbar navbar-toggleable-md navbar-light bg-faded">
  <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <a class="navbar-brand" href="#">Movies</a>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">New <span class="sr-only">(current)</span></a>
      </li>
    </ul>
    <form class="form-inline my-2 my-lg-0">
      <input class="form-control mr-sm-2" type="text" placeholder="Search ...">
      <button class="btn btn-outline-success my-2 my-sm-0" type="submit">GO !</button>
    </form>
  </div>
</nav>

打开navbar.component.ts并将选择器属性更新为movies-navbar。这里的选择器只是一个标签,可以用来引用其他组件上的组件:

@Component({
  selector: 'movies-navbar',
  templateUrl: './navbar.component.html',
  styleUrls: ['./navbar.component.css']
})
export class NavbarComponent implements OnInit {
   ...
}

movies-navbar选择器需要添加到app.component.html文件中,如下所示:

<movies-navbar></movies-navbar> 

Angular CLI 使用实时重新加载。因此,每当我们的代码更改时,CLI 将重新编译,重新注入(如果需要),并要求浏览器刷新页面:

当添加movies-navbar标签时,navbar.component.html文件中的所有内容都将显示在浏览器中。

同样,我们将为电影项目创建一个新组件:

ng generate component components/movie-item

我们将在界面中将电影显示为卡片;用以下内容替换movie-item.component.html代码:

<div class="card" style="width: 20rem;">
  <img class="card-img-top" src="img/185x287" alt="movie title">
  <div class="card-block">
    <h4 class="card-title">Movie</h4>
    <p class="card-text">Some quick description</p>
    <a href="#" class="btn btn-primary">Rent</a>
  </div>
</div>

在浏览器中,您应该看到类似于这样的东西:

创建另一个组件来显示电影列表:

ng generate component components/list-movies

该组件将使用 Angular 的ngFor指令来遍历movies数组中的movie并通过调用movie-item组件打印出电影(这称为组合):

<div class="row">
  <div class="col-sm-3" *ngFor="let movie of movies">
    <movie-item></movie-item>
  </div>
</div>

movies数组在list-movies.component.ts中声明,并在类构造函数中初始化:

import { Component, OnInit } from '@angular/core';
import { Movie } from '../../models/movie';

@Component({
  selector: 'list-movies',
  templateUrl: './list-movies.component.html',
  styleUrls: ['./list-movies.component.css']
})
export class ListMoviesComponent implements OnInit {

  public movies: Movie[];

  constructor() {
    this.movies = [
      new Movie("Avengers", "Some description", "https://image.tmdb.org/t/p/w370_and_h556_bestv2/cezWGskPY5x7GaglTTRN4Fugfb8.jpg"),
      new Movie("Thor", "Some description", "https://image.tmdb.org/t/p/w370_and_h556_bestv2/bIuOWTtyFPjsFDevqvF3QrD1aun.jpg"),
      new Movie("Spiderman", "Some description"),
    ]
  }

  ...

}

Movie类是一个简单的实体,有三个字段,即namecoverdescription,以及用于访问和修改类属性的 getter 和 setter:

export class Movie {
  private name: string;
  private cover: string;
  private description: string;

  constructor(name: string, description: string, cover?: string){
    this.name = name;
    this.description = description;
    this.cover = cover ? cover : "http://via.placeholder.com/185x287";
  }

  public getName(){
    return this.name;
  }

  public getCover(){
    return this.cover;
  }

  public getDescription(){
    return this.description;
  }

  public setName(name: string){
    this.name = name;
  }

  public setCover(cover: string){
    this.cover = cover;
  }

  public setDescription(description: string){
    this.description = description;
  }
}

如果我们运行上述代码,我们将在浏览器中看到三部电影:

到目前为止,电影属性在 HTML 页面中是硬编码的,为了改变这一点,我们需要将电影项目传递给movie-item元素。更新movie-item.component.ts以添加一个新的电影字段,并使用Input注释来使用 Angular 输入绑定:

export class MovieItemComponent implements OnInit {
  @Input()
  public movie: Movie;

  ...
}

在前面组件的 HTML 模板中,使用Movie类的 getter 来获取属性的值:

<div class="card">
    <img class="card-img-top" [src]="movie.getCover()" alt="{{movie.getName()}}">
    <div class="card-block">
      <h4 class="card-title">{{movie.getName()}}</h4>
      <p class="card-text">{{movie.getDescription()}}</p>
      <a href="#" class="btn btn-primary">Rent</a>
    </div>
</div>

最后,使ListMoviesComponentMovieItemComponent子嵌套在*ngFor重复器中,并在每次迭代中将movie实例绑定到子的movie属性上:

<div class="row">
  <div class="col-sm-3" *ngFor="let movie of movies">
    <movie-item [movie]="movie"></movie-item>
  </div>
</div>

在浏览器中,您应该确保电影的属性已经正确定义:

到目前为止一切都很顺利。但是,电影列表仍然是静态的和硬编码的。我们将通过调用无服务器 API 从数据库动态检索电影列表来解决这个问题。

使用 Angular 访问 Rest Web 服务

在前几章中,我们创建了两个阶段,即stagingproduction环境。因此,我们应该创建两个环境文件,以指向正确的 API Gateway 部署阶段:

  • environment.ts:包含开发 HTTP URL:
export const environment = {
  api: 'https://51cxzthvma.execute-api.us-east-1.amazonaws.com/staging/movies'
};
  • environment.prod.ts:包含生产 HTTP URL:
export const environment = {
  api: 'https://51cxzthvma.execute-api.us-east-1.amazonaws.com/production/movies'
};

如果执行ng buildng serveenvironment对象将从environment.ts中读取值,并且如果使用ng build --prod命令将应用程序构建为生产模式,则将从environment.prod.ts中读取值。

要创建服务,我们需要使用命令行。命令如下:

ng generate service services/moviesApi

movies-api.service.ts将实现findAll函数,该函数将使用Http服务调用 API Gateway 的findAll端点。map方法将帮助将响应转换为 JSON 格式:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import { environment } from '../../environments/environment';

@Injectable()
  export class MoviesApiService {

    constructor(private http:Http) { }

    findAll(){
      return this.http
      .get(environment.api)
      .map(res => {
        return res.json()
      })
    }

}

在调用MoviesApiService之前,需要在app.module.ts的主模块中的提供程序部分导入它。

更新MoviesListComponent以调用新服务。在浏览器控制台中,您应该会收到有关 Access-Control-Allow-Origin 头在 API Gateway 返回的响应中不存在的错误消息。这将是即将到来部分的主题:

跨域资源共享

出于安全目的,如果外部请求与您的网站的确切主机、协议和端口不匹配,浏览器将阻止流。在我们的示例中,我们有不同的域名(localhost 和 API Gateway URL)。

这种机制被称为同源策略。为了解决这个问题,您可以使用 CORS 头、代理服务器或 JSON 解决方法。在本节中,我将演示如何在 Lambda 函数返回的响应中使用 CORS 头来解决此问题:

  1. 修改findAllMovie函数的代码以添加Access-Control-Allow-Origin:*以允许来自任何地方的跨域请求(或指定域而不是*):
return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    },
    Body: string(response),
  }, nil
  1. 提交您的更改;应触发新的构建。在 CI/CD 管道的最后,FindAllMovies Lambda 函数的代码将被更新。测试一下;您应该会在headers属性的一部分中看到新的密钥:

  1. 如果刷新 Web 应用程序页面,JSON 对象也将显示在控制台中:

  1. 更新list-movies.component.ts以调用MoviesApiServicefindAll函数。返回的数据将存储在movies变量中:
constructor(private moviesApiService: MoviesApiService) {
  this.movies = []

  this.moviesApiService.findAll().subscribe(res => {
    res.forEach(movie => {
    this.movies.push(new Movie(movie.name, "Some description"))
    })
  })
}
  1. 结果,电影列表将被检索并显示:

  1. 我们没有封面图片;您可以更新 DynamoDB 的movies表以添加图像和描述属性:

NoSQL 数据库允许您随时更改表模式,而无需首先定义结构,而关系数据库则要求您使用预定义的模式来确定在处理数据之前的结构。

  1. 如果刷新 Web 应用程序页面,您应该可以看到带有相应描述和海报封面的电影:

  1. 通过实现新的电影功能来改进此 Web 应用程序。由于用户需要填写电影的图像封面和描述,因此我们需要更新insert Lambda 函数,以在后端生成随机唯一 ID 的同时添加封面和描述字段:
svc := dynamodb.New(cfg)
req := svc.PutItemRequest(&dynamodb.PutItemInput{
  TableName: aws.String(os.Getenv("TABLE_NAME")),
  Item: map[string]dynamodb.AttributeValue{
    "ID": dynamodb.AttributeValue{
      S: aws.String(uuid.Must(uuid.NewV4()).String()),
    },
    "Name": dynamodb.AttributeValue{
      S: aws.String(movie.Name),
    },
    "Cover": dynamodb.AttributeValue{
      S: aws.String(movie.Cover),
    },
    "Description": dynamodb.AttributeValue{
      S: aws.String(movie.Description),
    },
  },
})
  1. 一旦新更改被推送到代码存储库并部署,打开您的 REST 客户端并发出 POST 请求以添加新的电影,其 JSON 方案如下:

  1. 应返回200成功代码,并且在 Web 应用程序中应列出新电影:

单页应用程序部分所示,当用户点击“新建”按钮时,将弹出一个带有创建表单的模态框。为了构建这个模态框并避免使用 jQuery,我们将使用另一个库,该库提供了一组基于 Bootstrap 标记和 CSS 的本机 Angular 指令:

  • 使用以下命令安装此库:
npm install --save @ng-bootstrap/ng-bootstrap@2.0.0
  • 安装后,需要将其导入到主app.module.ts模块中,如下所示:
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';

@NgModule({
  declarations: [AppComponent, ...],
  imports: [NgbModule.forRoot(), ...],
  bootstrap: [AppComponent]
})
export class AppModule {
}
  • 为了容纳创建表单,我们需要创建一个新的组件:
ng generate component components/new-movie
  • 该组件将有两个用于电影标题和封面链接的input字段。另外,还有一个用于电影描述的textarea元素:
<div class="modal-header">
 <h4 class="modal-title">New Movie</h4>
 <button type="button" class="close" aria-label="Close" (click)="d('Cross click')">
 <span aria-hidden="true">&times;</span>
 </button>
</div>
<div class="modal-body">
 <div *ngIf="showMsg" class="alert alert-success" role="alert">
 <b>Well done !</b> You successfully added a new movie.
 </div>
 <div class="form-group">
 <label for="title">Title</label>
 <input type="text" class="form-control" #title>
 </div>
 <div class="form-group">
 <label for="description">Description</label>
 <textarea class="form-control" #description></textarea>
 </div>
 <div class="form-group">
 <label for="cover">Cover</label>
 <input type="text" class="form-control" #cover>
 </div>
</div>
<div class="modal-footer">
   <button type="button" class="btn btn-success" (click)="save(title.value, description.value, cover.value)">Save</button>
</div>
  • 用户每次点击保存按钮时,将响应点击事件调用save函数。MoviesApiService服务中定义的insert函数调用 API Gateway 的insert端点上的POST方法:
insert(movie: Movie){
  return this.http
    .post(environment.api, JSON.stringify(movie))
    .map(res => {
    return res
  })
}
  • 在导航栏中的 New 元素上添加点击事件:
<a class="nav-link" href="#" (click)="newMovie(content)">New <span class="badge badge-danger">+</span></a>
  • 点击事件将调用newMovie并通过调用ng-bootstrap库的ModalService模块打开模态框:
import { Component, OnInit, Input } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';

@Component({
 selector: 'movies-navbar',
 templateUrl: './navbar.component.html',
 styleUrls: ['./navbar.component.css']
})
export class NavbarComponent implements OnInit {

 constructor(private modalService: NgbModal) {}

 ngOnInit() {}

 newMovie(content){
 this.modalService.open(content);
 }

}
  • 一旦编译了这些更改,从导航栏中单击“新建”项目,模态框将弹出。填写必填字段,然后单击保存按钮:

  • 电影将保存在数据库表中。如果刷新页面,电影将显示在电影列表中:

S3 静态网站托管

现在我们的应用程序已创建,让我们将其部署到远程服务器。不要维护 Web 服务器,如 EC2 实例中的 Apache 或 Nginx,让我们保持无服务器状态,并使用启用了 S3 网站托管功能的 S3 存储桶。

设置 S3 存储桶

要开始,可以从 AWS 控制台或使用以下 AWS CLI 命令创建一个 S3 存储桶:

aws s3 mb s3://serverlessmovies.com

接下来,为生产模式构建 Web 应用程序:

ng build --prod

--prod标志将生成代码的优化版本,并执行额外的构建步骤,如 JavaScript 和 CSS 文件的最小化,死代码消除和捆绑:

这将为您提供dist/目录,其中包含index.html和所有捆绑的js文件,准备用于生产。配置存储桶以托管网站:

aws s3 website s3://serverlessmovies.com  -- index-document index.html

dist/文件夹中的所有内容复制到之前创建的 S3 存储桶中:

aws s3 cp --recursive dist/ s3://serverlessmovies.com/

您可以通过 S3 存储桶仪表板或使用aws s3 ls命令验证文件是否已成功存储:

默认情况下,创建 S3 存储桶时是私有的。因此,您应该使用以下存储桶策略使其公开访问:

{
  "Id": "Policy1529862214606",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1529862213126",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::serverlessmovies.com/*",
      "Principal": "*"
    }
  ]
}

在存储桶配置页面上,单击“权限”选项卡,然后单击“存储桶策略”,将策略内容粘贴到编辑器中,然后保存。将弹出警告消息,指示存储桶已变为公共状态:

要访问 Web 应用程序,请将浏览器指向serverlessmovies.s3-website-us-east-1.amazonaws.com(用您自己的存储桶名称替换):

现在我们的应用程序已部署到生产环境,让我们创建一个自定义域名,以便用户友好地访问网站。为了将域流量路由到 S3 存储桶,我们将使用Amazon Route 53创建一个指向存储桶的别名记录。

设置 Route 53

如果您是 Route 53 的新手,请使用您拥有的域名创建一个新的托管区域,如下图所示。您可以使用现有的域名,也可以从亚马逊注册商或 GoDaddy 等外部 DNS 注册商购买一个域名。确保选择公共托管区域:

创建后,NSSOA记录将自动为您创建。如果您从 AWS 购买了域名,您可以跳过此部分。如果没有,您必须更改您从域名注册商购买的域名的名称服务器记录。在本例中,我从 GoDaddy 购买了serverlessmovies.com/域名,因此在域名设置页面上,我已将名称服务器更改为 AWS 提供的NS记录值,如下所示:

更改可能需要几分钟才能传播。一旦由注册商验证,跳转到Route 53并创建一个新的A别名记录,该记录指向我们之前创建的 S3 网站,方法是从下拉列表中选择目标 S3 存储桶:

完成后,您将能够打开浏览器,输入您的域名,并查看您的 Web 应用程序:

拥有一个安全的网站可以产生差异,并使用户更加信任您的 Web 应用程序,这就是为什么在接下来的部分中,我们将使用 AWS 提供的免费 SSL 来显示自定义域名的内容,并使用HTTPS

证书管理器

您可以轻松地通过AWS 证书管理器(ACM)获得 SSL 证书。点击“请求证书”按钮创建一个新的 SSL 证书:

选择请求公共证书并添加您的域名。您可能还希望通过添加一个星号来保护您的子域:

在两个域名下,点击 Route 53 中的创建记录按钮。这将自动在 Route 53 中创建一个CNAME记录集,并由 ACM 检查以验证您拥有这些域:

一旦亚马逊验证域名属于您,证书状态将从“待验证”更改为“已签发”:

然而,我们无法配置 S3 存储桶以使用我们的 SSL 来加密流量。这就是为什么我们将在 S3 存储桶前使用一个 CloudFront 分发,也被称为 CDN。

CloudFront 分发

除了使用 CloudFront 在网站上添加 SSL 终止外,CloudFront 主要用作内容交付网络(CDN),用于在世界各地的多个边缘位置存储静态资产(如 HTML 页面、图像、字体、CSS 和 JavaScript),从而实现更快的下载和更短的响应时间。

也就是说,导航到 CloudFront,然后创建一个新的 Web 分发。在原始域名字段中设置 S3 网站 URL,并将其他字段保留为默认值。您可能希望将HTTP流量重定向到HTTPS

接下来,选择我们在证书管理器部分创建的 SSL 证书,并将您的域名添加到备用域名(CNAME)区域:

点击保存,并等待几分钟,让 CloudFront 复制所有文件到 AWS 边缘位置:

一旦 CDN 完全部署,跳转到域名托管区域页面,并更新网站记录以指向 CloudFront 分发域:

如果您再次转到 URL,您应该会被重定向到HTTPS

随意创建一个新的CNAME记录用于 API Gateway URL。该记录可能是api.serverlessmovies.com,指向51cxzthvma.execute-api.us-east-1.amazonaws.com/production/movies

CI/CD 工作流

我们的无服务器应用程序已部署到生产环境。但是,为了避免每次实现新功能时都重复相同的步骤,我们可以创建一个 CI/CD 流水线,自动化前一节中描述的工作流程。我选择 CircleCI 作为 CI 服务器。但是,您可以使用 Jenkins 或 CodePipeline——请确保阅读前几章以获取更多详细信息。

如前几章所示,流水线应该在模板文件中定义。以下是用于自动化 Web 应用程序部署流程的流水线示例:

version: 2
jobs:
  build:
    docker:
      - image: node:10.5.0

    working_directory: ~/serverless-movies

    steps:
      - checkout

      - restore_cache:
          key: node-modules-{{checksum "package.json"}}

      - run:
          name: Install dependencies
          command: npm install && npm install -g @angular/cli

      - save_cache:
          key: node-modules-{{checksum "package.json"}}
          paths:
            - node_modules

      - run:
          name: Build assets
          command: ng build --prod --aot false

      - run:
          name: Install AWS CLI
          command: |
            apt-get update
            apt-get install -y awscli

      - run:
          name: Push static files
          command: aws s3 cp --recursive dist/ s3://serverlessmovies.com/

以下步骤将按顺序执行:

  • 从代码存储库检出更改

  • 安装 AWS CLI,应用程序 npm 依赖项和 Angular CLI

  • 使用ng build命令构建工件

  • 将工件复制到 S3 存储桶

现在,您的 Web 应用程序代码的所有更改都将通过流水线进行,并将自动部署到生产环境:

API 文档

在完成本章之前,我们将介绍如何为迄今为止构建的无服务器 API 创建文档。

在 API Gateway 控制台上,选择要为其生成文档的部署阶段。在下面的示例中,我选择了production环境。然后,单击“导出”选项卡,单击“导出为 Swagger”部分:

Swagger 是OpenAPI的实现,这是 Linux Foundation 定义的关于如何描述和定义 API 的标准。这个定义被称为OpenAPI 规范文档

您可以将文档保存为 JSON 或 YAML 文件。然后,转到editor.swagger.io/并将内容粘贴到网站编辑器上,它将被编译,并生成一个 HTML 页面,如下所示:

AWS CLI 也可以用于导出 API Gateway 文档,使用aws apigateway get-export --rest-api-id API_ID --stage-name STAGE_NAME --export-type swagger swagger.json命令。

API Gateway 和 Lambda 函数与无服务器应用程序类似。可以编写 CI/CD 来自动化生成文档,每当在 API Gateway 上实现新的端点或资源时。流水线必须执行以下步骤:

  • 创建一个 S3 存储桶

  • 在存储桶上启用静态网站功能

  • github.com/swagger-api/swagger-ui下载 Swagger UI,并将源代码复制到 S3

  • 创建 DNS 记录(docs.serverlessmovies.com

  • 运行aws apigateway export命令生成 Swagger 定义文件

  • 使用aws s3 cp命令将spec文件复制到 S3

摘要

总之,我们已经看到了如何使用多个 Lambda 函数从头开始构建无服务器 API,以及如何使用 API Gateway 创建统一的 API 并将传入的请求分派到正确的 Lambda 函数。我们通过 DynamoDB 数据存储解决了 Lambda 的无状态问题,并了解了保留并发性如何帮助保护下游资源。然后,我们在 S3 存储桶中托管了一个无服务器 Web 应用程序,并在其前面使用 CloudFront 来优化 Web 资产的交付。最后,我们学习了如何使用 Route 53 将域流量路由到 Web 应用程序,并如何使用 SSL 终止来保护它。

以下图示了我们迄今为止实施的架构:

在下一章中,我们将改进 CI/CD 工作流程,添加单元测试和集成测试,以在将 Lambda 函数部署到生产环境之前捕获错误和问题。

问题

  1. 实现一个以电影类别为输入并返回与该类别对应的电影列表的 Lambda 函数。

  2. 实现一个 Lambda 函数,以电影标题作为输入,返回所有标题中包含关键字的电影。

  3. 在 Web 应用程序上实现一个删除按钮,通过调用 API Gateway 的DeleteMovie Lambda 函数来删除电影。

  4. 在 Web 应用程序上实现一个编辑按钮,允许用户更新电影属性。

  5. 使用 CircleCI、Jenkins 或 CodePipeline 实现 CI/CD 工作流程,自动化生成和部署 API Gateway 文档。

第十章:测试您的无服务器应用程序

本章将教您如何使用 AWS 无服务器应用程序模型在本地测试您的无服务器应用程序。我们还将介绍使用第三方工具进行 Go 单元测试和性能测试,以及如何使用 Lambda 本身执行测试工具。

技术要求

本章是第七章的后续内容,实施 CI/CD 流水线,因此建议先阅读该章节,以便轻松地跟随本章。此外,建议具有测试驱动开发实践经验。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go

单元测试

对 Lambda 函数进行单元测试意味着尽可能完全地(尽可能)从外部资源(如以下事件:DynamoDB、S3、Kinesis)中隔离测试函数处理程序。这些测试允许您在实际部署新更改到生产环境之前捕获错误,并维护源代码的质量、可靠性和安全性。

在我们编写第一个单元测试之前,了解一些关于 Golang 中测试的背景可能会有所帮助。要在 Go 中编写新的测试套件,文件名必须以_test.go结尾,并包含以TestFUNCTIONNAME前缀的函数。Test前缀有助于识别测试例程。以_test结尾的文件将在构建部署包时被排除,并且只有在发出go test命令时才会执行。此外,Go 自带了一个内置的testing包,其中包含许多辅助函数。但是,为了简单起见,我们将使用一个名为testify的第三方包,您可以使用以下命令安装:

go get -u github.com/stretchr/testify

以下是我们在上一章中构建的 Lambda 函数的示例,用于列出 DynamoDB 表中的所有电影。以下代表我们要测试的代码:

func findAll() (events.APIGatewayProxyResponse, error) {
  ...

  svc := dynamodb.New(cfg)
  req := svc.ScanRequest(&dynamodb.ScanInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
  })
  res, err := req.Send()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while scanning DynamoDB",
    }, nil
  }

  ...

  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    },
    Body: string(response),
  }, nil
}

为了充分覆盖代码,我们需要测试所有边缘情况。我们可以执行的测试示例包括:

  • 测试在未分配给函数的 IAM 角色的情况下的行为。

  • 使用分配给函数的 IAM 角色进行测试。

为了模拟 Lambda 函数在没有 IAM 角色的情况下运行,我们可以删除凭据文件或取消设置本地使用的 AWS 环境变量。然后,发出aws s3 ls命令以验证 AWS CLI 无法找到 AWS 凭据。如果看到以下消息,那么您应该可以继续:

Unable to locate credentials. You can configure credentials by running "aws configure".

在名为main_test.go的文件中编写您的单元测试:

package main

import (
  "net/http"
  "testing"

  "github.com/aws/aws-lambda-go/events"
  "github.com/stretchr/testify/assert"
)

func TestFindAll_WithoutIAMRole(t *testing.T) {
  expected := events.APIGatewayProxyResponse{
    StatusCode: http.StatusInternalServerError,
    Body: "Error while scanning DynamoDB",
  }
  response, err := findAll()
  assert.IsType(t, nil, err)
  assert.Equal(t, expected, response)
}

测试函数以Test关键字开头,后跟函数名称和我们要测试的行为。然后,它调用findAll处理程序并将实际结果与预期响应进行比较。然后,您可以按照以下步骤进行:

  1. 使用以下命令启动测试。该命令将查找当前文件夹中的任何文件中的任何测试并运行它们。确保设置TABLE_NAME环境变量:
TABLE_NAME=movies go test

太棒了!我们的测试有效,因为预期和实际响应体等于扫描 DynamoDB 时出错的值:

  1. 编写另一个测试函数,以验证如果在运行时将 IAM 角色分配给 Lambda 函数的处理程序行为:
package main

import (
  "testing"

  "github.com/stretchr/testify/assert"
)

func TestFindAll_WithIAMRole(t *testing.T) {
  response, err := findAll()
  assert.IsType(t, nil, err)
  assert.NotNil(t, response.Body)
}

再次,测试应该通过,因为预期和实际响应体不为空:

您现在已经在 Go 中运行了一个单元测试;让我们为期望输入参数的 Lambda 函数编写另一个单元测试。让我们以insert方法为例。我们要测试的代码如下(完整代码可以在 GitHub 存储库中找到):

func insert(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  ...
  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    },
  }, nil
}

这种情况是输入参数的无效有效负载。函数应返回带有Invalid payload消息的400错误:

func TestInsert_InvalidPayLoad(t *testing.T) {
  input := events.APIGatewayProxyRequest{
    Body: "{'name': 'avengers'}",
  }

  expected := events.APIGatewayProxyResponse{
    StatusCode: 400,
    Body: "Invalid payload",
  }
  response, _ := insert(input)
  assert.Equal(t, expected, response)
}

另一个用例是在给定有效负载的情况下,函数应将电影插入数据库并返回200成功代码:

func TestInsert_ValidPayload(t *testing.T) {
  input := events.APIGatewayProxyRequest{
    Body: "{\"id\":\"40\", \"name\":\"Thor\", \"description\":\"Marvel movie\", \"cover\":\"poster url\"}",
  }
  expected := events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    },
  }
  response, _ := insert(input)
  assert.Equal(t, expected, response)
}

两个测试应该成功通过。这次,我们将以代码覆盖模式运行go test命令,使用-cover标志:

TABLE_NAME=movies go test -cover

我们有 78%的代码被单元测试覆盖:

如果您想深入了解测试覆盖了哪些语句,哪些没有,可以使用以下命令生成 HTML 覆盖报告:

TABLE_NAME=movies go test -cover -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html

如果在浏览器中打开coverage.html,您可以看到单元测试未覆盖的语句:

您可以通过利用 Go 的接口来改进单元测试,以模拟 DynamoDB 调用。这允许您模拟 DynamoDB 的实现,而不是直接使用具体的服务客户端(例如,aws.amazon.com/blogs/developer/mocking-out-then-aws-sdk-for-go-for-unit-testing/)。

自动化单元测试

拥有单元测试是很好的。然而,没有自动化的单元测试是没有用的,因此您的 CI/CD 流水线应该有一个测试阶段,以执行对代码存储库提交的每个更改的单元测试。这种机制有许多好处,例如确保您的代码库处于无错误状态,并允许开发人员持续检测和修复集成问题,从而避免在发布日期上出现最后一分钟的混乱。以下是我们在前几章中构建的自动部署 Lambda 函数的流水线的示例:

version: 2
jobs:
 build:
 docker:
 - image: golang:1.8

 working_directory: /go/src/github.com/mlabouardy/lambda-circleci

 environment:
 S3_BUCKET: movies-api-deployment-packages
 TABLE_NAME: movies
 AWS_REGION: us-east-1

 steps:
 - checkout

 - run:
 name: Install AWS CLI & Zip
 command: |
 apt-get update
 apt-get install -y zip python-pip python-dev
 pip install awscli

 - run:
 name: Test
 command: |
 go get -u github.com/golang/lint/golint
 go get -t ./...
 golint -set_exit_status
 go vet .
 go test .

 - run:
 name: Build
 command: |
 GOOS=linux go build -o main main.go
 zip $CIRCLE_SHA1.zip main

 - run:
 name: Push
 command: aws s3 cp $CIRCLE_SHA1.zip s3://$S3_BUCKET

 - run:
 name: Deploy
 command: |
 aws lambda update-function-code --function-name InsertMovie \
 --s3-bucket $S3_BUCKET \
 --s3-key $CIRCLE_SHA1.zip --region us-east-1

对 Lambda 函数源代码的所有更改都将触发新的构建,并重新执行单元测试:

如果单击“Test”阶段,您将看到详细的go test命令结果:

集成测试

与单元测试不同,单元测试测试系统的一个单元,集成测试侧重于作为一个整体测试 Lambda 函数。那么,在不将它们部署到 AWS 的本地开发环境中如何测试 Lambda 函数呢?继续阅读以了解更多信息。

RPC 通信

如果您阅读 AWS Lambda 的官方 Go 库(github.com/aws/aws-lambda-go)的底层代码,您会注意到基于 Go 的 Lambda 函数是使用net/rpc通过TCP调用的。每个 Go Lambda 函数都会在由_LAMBDA_SERVER_PORT环境变量定义的端口上启动服务器,并等待传入请求。为了与函数交互,使用了两个 RPC 方法:

  • Ping:用于检查函数是否仍然存活和运行

  • Invoke:用于执行请求

有了这些知识,我们可以模拟 Lambda 函数的执行,并执行集成测试或预部署测试,以减少将函数部署到 AWS 之前的等待时间。我们还可以在开发生命周期的早期阶段修复错误,然后再将新更改提交到代码存储库。

以下示例是一个简单的 Lambda 函数,用于计算给定数字的 Fibonacci 值。斐波那契数列是前两个数字的和。以下代码是使用递归实现的斐波那契数列:

package main

import "github.com/aws/aws-lambda-go/lambda"

func fib(n int64) int64 {
  if n > 2 {
    return fib(n-1) + fib(n-2)
  }
  return 1
}

func handler(n int64) (int64, error) {
  return fib(n), nil
}

func main() {
  lambda.Start(handler)
}

Lambda 函数通过 TCP 监听端口,因此我们需要通过设置_LAMBDA_SERVER_PORT环境变量来定义端口:

_LAMBDA_SERVER_PORT=3000 go run main.go

要调用函数,可以使用net/rpc go 包中的invoke方法,也可以安装一个将 RPC 通信抽象为单个方法的 Golang 库:

go get -u github.com/djhworld/go-lambda-invoke 

然后,通过设置运行的端口和要计算其斐波那契数的数字来调用函数:

package main

import (
  "fmt"
  "log"

  "github.com/djhworld/go-lambda-invoke/golambdainvoke"
)

func main() {
  response, err := golambdainvoke.Run(3000, 9)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(string(response))
}

使用以下命令调用 Fibonacci Lambda 函数:

go run client.go

结果,fib(9)=34如预期返回:

另一种方法是使用net/http包构建 HTTP 服务器,模拟 Lambda 函数在 API Gateway 后面运行,并以与测试任何 HTTP 服务器相同的方式测试函数,以验证处理程序。

在下一节中,我们将看到如何使用 AWS 无服务器应用程序模型以更简单的方式在本地测试 Lambda 函数。

无服务器应用程序模型

无服务器应用程序模型SAM)是一种在 AWS 中定义无服务器应用程序的方式。它是对CloudFormation的扩展,允许在模板文件中定义运行函数所需的所有资源。

请参阅第十四章,基础设施即代码,了解如何使用 SAM 从头开始构建无服务器应用程序的说明。

此外,AWS SAM 允许您创建一个开发环境,以便在本地测试、调试和部署函数。执行以下步骤:

  1. 要开始,请使用pip Python 包管理器安装 SAM CLI:
pip install aws-sam-cli

确保安装所有先决条件,并确保 Docker 引擎正在运行。有关更多详细信息,请查看官方文档docs.aws.amazon.com/lambda/latest/dg/sam-cli-requirements.html

  1. 安装后,运行sam --version。如果一切正常,它应该输出 SAM 版本(在撰写本书时为v0.4.0)。

  2. 为 SAM CLI 创建template.yml,在其中我们将定义运行函数所需的运行时和资源:

AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: List all movies.
Resources:
 FindAllMovies:
 Type: AWS::Serverless::Function
 Properties:
 Handler: main
 Runtime: go1.x
 Events:
 Vote:
 Type: Api
 Properties:
 Path: /movies
 Method: get

SAM 文件描述了运行时环境和包含代码的处理程序的名称,当调用时,Lambda 函数将执行该代码。此外,模板定义了将触发函数的事件;在本例中,它是 API Gateway 端点。

  • 为 Linux 构建部署包:
GOOS=linux go build -o main
  • 在本地使用sam local命令运行函数:
sam local start-api

HTTP 服务器将在端口3000上运行并侦听:

如果您导航到http://localhost:3000/movies,在返回响应之前可能需要几分钟,因为它需要获取一个 Docker 镜像:

SAM 本地利用容器的强大功能在 Docker 容器中运行 Lambda 函数的代码。在前面的屏幕截图中,它正在从 DockerHub(一个镜像存储库)拉取lambci/lambda:go1.x Docker 镜像。您可以通过运行以下命令来列出机器上所有可用的镜像来确认:

docker image ls

以下是前面命令的输出:

一旦拉取了镜像,将基于您的deployment包创建一个新的容器:

在浏览器中,将显示错误消息,因为我们忘记设置 DynamoDB 表的名称:

我们可以通过创建一个env.json文件来解决这个问题,如下所示:

{
    "FindAllMovies" : {
        "TABLE_NAME" : "movies"
    }
}

使用--env-var参数运行sam命令:

sam local start-api --env-vars env.json

您还可以在同一 SAM 模板文件中使用Environment属性声明环境变量。

这次,您应该在 DynamoDB movies表中拥有所有电影,并且函数应该按预期工作:

负载测试

我们已经看到了如何使用基准测试工具,例如 Apache Benchmark,以及如何测试测试工具。在本节中,我们将看看如何使用 Lambda 本身作为无服务器测试测试平台。

这个想法很简单:我们将编写一个 Lambda 函数,该函数将调用我们想要测试的 Lambda 函数,并将其结果写入 DynamoDB 表进行报告。幸运的是,这里不需要编码,因为 Lambda 函数已经在蓝图部分中可用:

为函数命名并创建一个新的 IAM 角色,如下图所示:

单击“创建函数”,函数应该被创建,并授予执行以下操作的权限:

  • 将日志推送到 CloudWatch。

  • 调用其他 Lambda 函数。

  • 向 DynamoDB 表写入数据。

以下截图展示了前面任务完成后的情况:

在启动负载测试之前,我们需要创建一个 DynamoDB 表,Lambda 将在其中记录测试的输出。该表必须具有testId的哈希键字符串和iteration的范围数字:

创建后,使用以下 JSON 模式调用 Lambda 函数。它将异步调用给定函数 100 次。指定一个唯一的event.testId来区分每个单元测试运行:

{
    "operation": "load",
    "iterations": 100,
    "function": "HarnessTestFindAllMovies",
    "event": {
      "operation": "unit",
      "function": "FindAllMovies",
      "resultsTable": "load-test-results",
      "testId": "id",
      "event": {
        "options": {
          "host": "https://51cxzthvma.execute-api.us-east-1.amazonaws.com",
          "path": "/production/movies",
          "method": "GET"
        }
      }
    }
  }

结果将记录在 JSON 模式中给出的 DynamoDB 表中:

您可能需要修改函数的代码以保存其他信息,例如运行时间、资源使用情况和响应时间。

摘要

在本章中,我们学习了如何为 Lambda 函数编写单元测试,以覆盖函数的所有边缘情况。我们还学习了如何使用 AWS SAM 设置本地开发环境,以在本地测试和部署函数,以确保其行为在部署到 AWS Lambda 之前正常工作。

在下一章中,我们将介绍如何使用 AWS 托管的服务(如 CloudWatch 和 X-Ray)来排除故障和调试无服务器应用程序。

问题

  1. UpdateMovie Lambda 函数编写一个单元测试。

  2. DeleteMovie Lambda 函数编写一个单元测试。

  3. 修改前几章提供的Jenkinsfile,以包括自动化单元测试的执行。

  4. 修改buildspec.yml定义文件,以在将部署包推送到 S3 之前,包括执行单元测试的执行。

  5. 为前几章实现的每个 Lambda 函数编写一个 SAM 模板文件。

第十一章:监控和故障排除

Lambda 监控与传统应用程序监控不同,因为您不管理代码运行的基础基础设施。因此,无法访问 OS 指标。但是,您仍然需要函数级别的监控来优化函数性能,并在发生故障时进行调试。在本章中,您将学习如何实现这一点,以及如何在 AWS 中调试和故障排除无服务器应用程序。您将学习如何基于 CloudWatch 中的指标阈值设置警报,以便在可能出现问题时收到通知。您还将了解如何使用 AWS X-Ray 对应用程序进行分析,以检测异常行为。

使用 AWS CloudWatch 进行监控和调试

AWS CloudWatch 是监控 AWS 服务的最简单和最可靠的解决方案,包括 Lambda 函数。它是一个集中的监控服务,用于收集指标和日志,并根据它们创建警报。AWS Lambda 会自动代表您监视 Lambda 函数,并通过 CloudWatch 报告指标。

CloudWatch 指标

默认情况下,每次通过 Lambda 控制台调用函数时,它都会报告有关函数资源使用情况、执行持续时间以及计费时间的关键信息:

单击“监控”选项卡可以快速实时了解情况。此页面将显示多个 CloudWatch 指标的图形表示。您可以在图形区域的右上角控制可观察时间段:

这些指标包括:

  • 函数被调用的次数

  • 执行时间(毫秒)

  • 错误率和由于并发预留和未处理事件(死信错误)而导致的节流计数

在 CloudWatch 中为 AWS Lambda 提供的所有可用指标列表可以在docs.aws.amazon.com/lambda/latest/dg/monitoring-functions-metrics.html找到。

对于每个指标,您还可以单击“在指标中查看”直接查看 CloudWatch 指标:

前面的图表表示在过去 15 分钟内productionstaging别名的FindAllMovies函数的调用次数。您可以进一步创建自定义图表。这使您可以为 Lambda 函数构建自定义仪表板。它将概述负载(您可能会遇到的任何问题)、成本和其他重要指标。

此外,您还可以使用 CloudWatch Golang SDK 创建自定义指标并将其发布到 CloudWatch。以下代码片段是使用 CloudWatch SDK 发布自定义指标的 Lambda 函数。该指标表示插入到 DynamoDB 中的Action电影的数量(为简洁起见,某些部分被省略):

svc := cloudwatch.New(cfg)
req := svc.PutMetricDataRequest(&cloudwatch.PutMetricDataInput{
  Namespace: aws.String("InsertMovie"),
  MetricData: []cloudwatch.MetricDatum{
    cloudwatch.MetricDatum{
      Dimensions: []cloudwatch.Dimension{
        cloudwatch.Dimension{
          Name: aws.String("Environment"),
          Value: aws.String("production"),
        },
      },
      MetricName: aws.String("ActionMovies"),
      Value: aws.Float64(1.0),
      Unit: cloudwatch.StandardUnitCount,
    },
  },
})

该指标由名称、命名空间、维度列表(名称-值对)、值和度量单位唯一定义。在您向 CloudWatch 发布了一些值之后,您可以使用 CloudWatch 控制台查看统计图表:

现在我们知道如何使用 AWS 提供的现成指标监视我们的 Lambda 函数,并将自定义指标插入到 CloudWatch 中以丰富它们的可观察性。让我们看看如何基于这些指标创建警报,以便在 Lambda 函数出现问题时实时通知我们。

CloudWatch 警报

CloudWatch 允许您在发生意外行为时基于可用指标创建警报。在以下示例中,我们将基于FindAllMovies函数的错误率创建警报:

为了实现这一点,请点击“操作”列中的铃铛图标。然后,填写以下字段以设置一个警报,如果在五分钟内错误数量超过10,则会触发警报。一旦触发警报,将使用简单通知服务SNS)发送电子邮件:

CloudWatch 将通过 SNS 主题发送通知,您可以创建尽可能多的 SNS 主题订阅,以便将通知传递到您想要的位置(短信、HTTP、电子邮件)。

点击“创建警报”按钮;您应该收到一封确认订阅的电子邮件。您必须在通知发送之前确认订阅:

一旦确认,每当 Lambda 函数的错误率超过定义的阈值时,警报将从“正常”状态更改为“警报”状态:

之后,将会向您发送一封电子邮件作为事件的响应:

您可以通过使用此 AWS CLI 命令临时更改其状态来模拟警报:aws cloudwatch set-alarm-state --alarm-name ALARM_NAME --state-value ALARM --state-reason demo

CloudWatch 日志

在使用 AWS Lambda 时,当函数被调用时,您可能会遇到以下错误:

  • 应用程序错误

  • 权限被拒绝

  • 超时

  • 内存超限

除了第一个用例外,其余的都可以通过授予正确的 IAM 策略并增加 Lambda 函数的超时或内存使用量来轻松解决。然而,第一个错误需要更多的调试和故障排除,这需要在代码中添加日志记录语句来验证您的代码是否按预期工作。幸运的是,每当 Lambda 函数的代码响应事件执行时,它都会将日志条目写入与 Lambda 函数关联的 CloudWatch 日志组,即/aws/lambda/FUNCTION_NAME

为了实现这一点,您的 Lambda 函数应被授予以下权限:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "1",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:CreateLogGroup",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

也就是说,您可以使用 Go 的内置日志记录库,称为log包。以下是如何使用log包的示例:

package main

import (
  "log"

  "github.com/aws/aws-lambda-go/lambda"
)

func reverse(s string) string {
  runes := []rune(s)
  for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
    runes[i], runes[j] = runes[j], runes[i]
  }
  return string(runes)
}

func handler(input string) (string, error) {
  log.Println("Before:", input)
  output := reverse(input)
  log.Println("After:", output)
  return output, nil
}

func main() {
  lambda.Start(handler)
}

代码是不言自明的,它对给定字符串执行了一个反向操作。我已经使用log.Println方法在代码的各个部分周围添加了日志记录语句。

然后,您可以将函数部署到 AWS Lambda,并从 AWS 控制台或使用invoke命令调用它。Lambda 会自动集成到 Amazon CloudWatch 日志,并将代码中的所有日志推送到与 Lambda 函数关联的 CloudWatch 日志组:

到目前为止,我们已经学会了如何通过日志和运行时数据来排除故障和分析每次调用。在接下来的部分中,我们将介绍如何在 Lambda 函数的代码中跟踪所有上游和下游对外部服务的调用,以便快速轻松地排除错误。为了跟踪所有这些调用,我们将在实际工作执行的不同代码段中使用 AWS X-Ray 添加代码仪器。

有许多第三方工具可用于监视无服务器应用程序,这些工具依赖于 CloudWatch。因此,它们在实时问题上也会失败。我们期望这在未来会得到解决,因为 AWS 正在以快速的速度推出新的服务和功能。

使用 AWS X-Ray 进行跟踪

AWS X-Ray 是 AWS 管理的服务,允许您跟踪 Lambda 函数发出的传入和传出请求。它将这些信息收集在段中,并使用元数据记录附加数据,以帮助您调试、分析和优化函数。

总的来说,X-Ray 可以帮助您识别性能瓶颈。然而,它可能需要在函数执行期间进行额外的网络调用,增加用户面对的延迟。

要开始,请从 Lambda 函数的配置页面启用主动跟踪:

要求以下 IAM 策略以使 Lambda 函数发布跟踪段到 X-Ray:

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": [
      "xray:PutTraceSegments",
      "xray:PutTelemetryRecords"
    ],
    "Resource": [
      "*"
    ]
  }
}

接下来,转到 AWS X-Ray 控制台,单击“跟踪”,多次调用 Lambda 函数,并刷新页面。将在跟踪列表中添加新行。对于每个跟踪,您将获得代码响应和执行时间:

这是FindAllMovies函数的跟踪;它包括 Lambda 初始化函数所需的时间:

您还可以通过单击“服务映射”项以图形格式可视化此信息:

对于每个被跟踪的调用,Lambda 将发出 Lambda 服务段和其所有子段。此外,Lambda 将发出 Lambda 函数段和 init 子段。这些段将被发出,而无需对函数的运行时进行任何更改或需要任何其他库。如果要使 Lambda 函数的 X-Ray 跟踪包括用于下游调用的自定义段、注释或子段,可能需要安装以下 X-Ray Golang SDK:

go get -u github.com/aws/aws-xray-sdk-go/...

更新FindAllMovies函数的代码以使用Configure方法配置 X-Ray:

xray.Configure(xray.Config{
  LogLevel: "info",
  ServiceVersion: "1.2.3",
})

我们将通过使用xray.AWS调用包装 DynamoDB 客户端来在子段中跟踪对 DynamoDB 的调用,如下面的代码所示:

func findAll(ctx context.Context) (events.APIGatewayProxyResponse, error) {
  xray.Configure(xray.Config{
    LogLevel: "info",
    ServiceVersion: "1.2.3",
  })

  sess := session.Must(session.NewSession())
  dynamo := dynamodb.New(sess)
  xray.AWS(dynamo.Client)

  res, err := dynamo.ScanWithContext(ctx, &dynamodb.ScanInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
  })

  ...
}

再次在 X-Ray“跟踪”页面上调用 Lambda 函数;将添加一个新的子段,显示它扫描movies表所花费的时间:

DynamoDB 调用还将显示为 X-Ray 控制台中服务映射上的下游节点:

现在我们已经熟悉了 X-Ray 的工作原理,让我们创建一些复杂的东西。考虑一个简单的 Lambda 函数,它以电影海报页面的 URL 作为输入。它解析 HTML 页面,提取数据,并将其保存到 DynamoDB 表中。此函数将在给定 URL 上执行GET方法:

res, err := http.Get(url)
if err != nil {
  log.Fatal(err)
}
defer res.Body.Close()

然后,它使用goquery库(JQuery Go 的实现)从 HTML 页面中提取数据,使用 CSS 选择器:

doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
  log.Fatal(err)
}

title := doc.Find(".header .title span a h2").Text()
description := doc.Find(".overview p").Text()
cover, _ := doc.Find(".poster .image_content img").Attr("src")

movie := Movie{
  ID: uuid.Must(uuid.NewV4()).String(),
  Name: title,
  Description: description,
  Cover: cover,
}

创建电影对象后,它使用PutItem方法将电影保存到 DynamoDB 表:

sess := session.Must(session.NewSession())
dynamo := dynamodb.New(sess)
req, _ := dynamo.PutItemRequest(&dynamodb.PutItemInput{
  TableName: aws.String(os.Getenv("TABLE_NAME")),
  Item: map[string]*dynamodb.AttributeValue{
    "ID": &dynamodb.AttributeValue{
      S: aws.String(movie.ID),
    },
    "Name": &dynamodb.AttributeValue{
      S: aws.String(movie.Name),
    },
    "Cover": &dynamodb.AttributeValue{
      S: aws.String(movie.Cover),
    },
    "Description": &dynamodb.AttributeValue{
      S: aws.String(movie.Description),
    },
  },
})
err = req.Send()
if err != nil {
  log.Fatal(err)
}

现在我们的函数处理程序已定义,将其部署到 AWS Lambda,并通过将 URL 作为输入参数进行测试。结果,电影信息将以 JSON 格式显示:

如果您将浏览器指向前几章构建的前端,新电影应该是页面上列出的电影之一:

现在我们的 Lambda 函数正在按预期工作;让我们为下游服务添加跟踪调用。首先,配置 X-Ray 并使用ctxhttp.Get方法将GET调用作为子段进行检测:

xray.Configure(xray.Config{
  LogLevel: "info",
  ServiceVersion: "1.2.3",
})

// Get html page
res, err := ctxhttp.Get(ctx, xray.Client(nil), url)
if err != nil {
  log.Fatal(err)
}
defer res.Body.Close()

接下来,在解析逻辑周围创建一个子段。子段称为Parsing,并且使用AddMetaData方法记录有关子段的其他信息以进行故障排除:

xray.Capture(ctx, "Parsing", func(ctx1 context.Context) error {
  doc, err := goquery.NewDocumentFromReader(res.Body)
  if err != nil {
    return err
  }

  title := doc.Find(".header .title span a h2").Text()
  description := doc.Find(".overview p").Text()
  cover, _ := doc.Find(".poster .image_content img").Attr("src")

  movie := Movie{
    ID: uuid.Must(uuid.NewV4()).String(),
    Name: title,
    Description: description,
    Cover: cover,
  }

  xray.AddMetadata(ctx1, "movie.title", title)
  xray.AddMetadata(ctx1, "movie.description", description)
  xray.AddMetadata(ctx1, "movie.cover", cover)

  return nil
})

最后,使用xray.AWS()调用包装 DynamoDB 客户端:

sess := session.Must(session.NewSession())
dynamo := dynamodb.New(sess)
xray.AWS(dynamo.Client)

结果,ParseMovies Lambda 函数的以下子段将出现在跟踪中:

如果单击“子段”-“解析”选项卡上的“元数据”,将显示电影属性如下:

在服务映射上,将显示对 DynamoDB 的下游调用和出站 HTTP 调用:

到目前为止,您应该清楚如何轻松排除性能瓶颈、延迟峰值和其他影响基于 Lambda 的应用程序性能的问题。

当您跟踪 Lambda 函数时,X-Ray 守护程序将自动在 Lambda 环境中运行,收集跟踪数据并将其发送到 X-Ray。如果您想在将函数部署到 Lambda 之前测试函数,可以在本地运行 X-Ray 守护程序。安装指南可以在这里找到:docs.aws.amazon.com/xray/latest/devguide/xray-daemon-local.html

摘要

在本章中,您学习了如何使用 AWS CloudWatch 指标实时监控 Lambda 函数。您还学习了如何发布自定义指标,并使用警报和报告检测问题。此外,我们还介绍了如何将函数的代码日志流式传输到 CloudWatch。最后,我们看到了如何使用 AWS X-Ray 进行调试,如何跟踪上游和下游调用,以及如何在 Golang 中将 X-Ray SDK 与 Lambda 集成。

在下一章中,您将学习如何保护您的无服务器应用程序。

第十二章:保护您的无服务器应用程序

AWS Lambda 是终极的按需付费云计算服务。客户只需将他们的 Lambda 函数代码上传到云端,它就可以运行,而无需保护或修补底层基础设施。然而,根据 AWS 的共享责任模型,您仍然负责保护您的 Lambda 函数代码。本章专门讨论在 AWS Lambda 中可以遵循的最佳实践和建议,以使应用程序根据 AWS Well-Architected Framework 具有弹性和安全性。本章将涵盖以下主题:

  • 身份验证和用户控制访问

  • 加密环境变量

  • 使用 CloudTrail 记录 AWS Lambda API 调用

  • 扫描依赖项的漏洞

技术要求

为了遵循本章,您可以遵循 API Gateway 设置章节,或者基于 Lambda 和 API Gateway 的无服务器 RESTful API。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Serverless-Applications-with-Go

身份验证和用户控制访问

到目前为止,我们构建的无服务器应用程序运行良好,并向公众开放。只要有 API Gateway 调用 URL,任何人都可以调用 Lambda 函数。幸运的是,AWS 提供了一个名为 Cognito 的托管服务。

Amazon Cognito是一个规模化的身份验证提供程序和管理服务,允许您轻松地向您的应用程序添加用户注册和登录。用户存储在一个可扩展的目录中,称为用户池。在即将到来的部分中,Amazon Cognito 将用于在允许他们请求 RESTful API 之前对用户进行身份验证。

要开始,请在 Amazon Cognito 中创建一个新的用户池并为其命名:

单击“审阅默认值”选项以使用默认设置创建池:

从导航窗格中单击“属性”,并在“电子邮件地址或电话号码”下的“允许电子邮件地址”选项中选中以允许用户使用电子邮件地址登录:

返回到“审阅”并单击“创建池”。创建过程结束时应显示成功消息:

创建第一个用户池后,从“常规设置”下的应用程序客户端中注册您的无服务器 API,并选择“添加应用程序客户端”。给应用程序命名,并取消“生成客户端密钥”选项如下:身份验证将在客户端上完成。因此,出于安全目的,客户端密钥不应传递到 URL 上:

选择“创建应用程序客户端”以注册应用程序,并将应用程序客户端 ID复制到剪贴板:

现在用户池已创建,我们可以配置 API Gateway 以在授予对 Lambda 函数的访问之前验证来自成功用户池身份验证的访问令牌。

保护 API 访问

要开始保护 API 访问,请转到 API Gateway 控制台,选择我们在前几章中构建的 RESTful API,并从导航栏中单击“授权者”:

单击“创建新的授权者”按钮,然后选择 Cognito。然后,选择我们之前创建的用户池,并将令牌源字段设置为Authorization。这定义了包含 API 调用者身份令牌的传入请求标头的名称为Authorization

填写完表单后,单击“创建”以将 Cognito 用户池与 API Gateway 集成:

现在,您可以保护所有端点,例如,为了保护负责列出所有电影的端点。点击/movies资源下的相应GET方法:

点击 Method Request 框,然后点击 Authorization,并选择我们之前创建的用户池:

将 OAuth Scopes 选项保留为None,并为其余方法重复上述过程以保护它们:

完成后,重新部署 API,并将浏览器指向 API Gateway 调用 URL:

这次,端点是受保护的,需要进行身份验证。您可以通过检查我们之前构建的前端来确认行为。如果检查网络请求,API Gateway 请求应返回 401 未经授权错误:

为了修复此错误,我们需要更新客户端(Web 应用程序)执行以下操作:

  • 使用 Cognito JavaScript SDK 登录用户池

  • 从用户池中获取已登录用户的身份令牌

  • 在 API Gateway 请求的 Authorization 标头中包含身份令牌

返回的身份令牌具有 1 小时的过期日期。一旦过期,您需要使用刷新令牌来刷新会话。

使用 AWS Cognito 进行用户管理

在客户端进行更改之前,我们需要在 Amazon Cognito 中创建一个测试用户。为此,您可以使用 AWS 管理控制台,也可以使用 AWS Golang SDK 以编程方式完成。

通过 AWS 管理控制台设置测试用户

点击用户和组,然后点击创建用户按钮:

设置用户名和密码。如果要收到确认电子邮件,可以取消选中“标记电子邮件为已验证?”框:

使用 Cognito Golang SDK 进行设置

创建一个名为main.go的文件,内容如下。该代码使用cognitoidentityprovider包中的SignUpRequest方法来创建一个新用户。作为参数,它接受一个包含客户端 ID、用户名和密码的结构体:

package main

import (
  "log"
  "os"

  "github.com/aws/aws-sdk-go-v2/aws/external"
  "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
  "github.com/aws/aws-sdk-go/aws"
)

func main() {
  cfg, err := external.LoadDefaultAWSConfig()
  if err != nil {
    log.Fatal(err)
  }

  cognito := cognitoidentityprovider.New(cfg)
  req := cognito.SignUpRequest(&cognitoidentityprovider.SignUpInput{
    ClientId: aws.String(os.Getenv("COGNITO_CLIENT_ID")),
    Username: aws.String("EMAIL"),
    Password: aws.String("PASSWORD"),
  })
  _, err = req.Send()
  if err != nil {
    log.Fatal(err)
  }
}

使用go run main.go命令运行上述命令。您将收到一封带有临时密码的电子邮件:

注册后,用户必须通过输入通过电子邮件发送的代码来确认注册。要确认注册过程,必须收集用户收到的代码并使用如下方式:

cognito := cognitoidentityprovider.New(cfg)
req := cognito.ConfirmSignUpRequest(&cognitoidentityprovider.ConfirmSignUpInput{
  ClientId: aws.String(os.Getenv("COGNITO_CLIENT_ID")),
  Username: aws.String("EMAIL"),
  ConfirmationCode: aws.String("CONFIRMATION_CODE"),
})
_, err = req.Send()
if err != nil {
  log.Fatal(err)
}

现在 Cognito 用户池中已创建了一个用户,我们准备更新客户端。首先创建一个登录表单如下:

接下来,使用 Node.js 包管理器安装 Cognito SDK for Javascript。该软件包包含与 Cognito 交互所需的 Angular 模块和提供程序:

npm install --save amazon-cognito-identity-js

此外,我们还需要创建一个带有auth方法的 Angular 服务,该方法通过提供UserPoolId对象和ClientId创建一个CognitoUserPool对象,根据参数中给定的用户名和密码对用户进行身份验证。如果登录成功,将调用onSuccess回调。如果登录失败,将调用onFailure回调:

import { Injectable } from '@angular/core';
import { CognitoUserPool, CognitoUser, AuthenticationDetails} from 'amazon-cognito-identity-js';
import { environment } from '../../environments/environment';

@Injectable()
export class CognitoService {

  public static CONFIG = {
    UserPoolId: environment.userPoolId,
    ClientId: environment.clientId
  }

  auth(username, password, callback){
    let user = new CognitoUser({
      Username: username,
      Pool: this.getUserPool()
    })

    let authDetails = new AuthenticationDetails({
      Username: username,
      Password: password
    })

    user.authenticateUser(authDetails, {
      onSuccess: res => {
        callback(null, res.getIdToken().getJwtToken())
      },
      onFailure: err => {
        callback(err, null)
      }
    })
  }

  getUserPool() {
    return new CognitoUserPool(CognitoService.CONFIG);
  }

  getCurrentUser() {
    return this.getUserPool().getCurrentUser();
  }

}

每次单击登录按钮时都会调用auth方法。如果用户输入了正确的凭据,将会与 Amazon Cognito 服务建立用户会话,并将用户身份令牌保存在浏览器的本地存储中。如果输入了错误的凭据,将向用户显示错误消息:

signin(username, password){
    this.cognitoService.auth(username, password, (err, token) => {
      if(err){
        this.loginError = true
      }else{
        this.loginError = false
        this.storage.set("COGNITO_TOKEN", token)
        this.loginModal.close()
      }
    })
  }

最后,MoviesAPI服务应更新以在每个 API Gateway 请求调用的Authorization头中包含用户身份令牌(称为 JWT 令牌 - docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html#amazon-cognito-user-pools-using-the-id-token)。

@Injectable()
export class MoviesApiService {

  constructor(private http: Http,
    @Inject(LOCAL_STORAGE) private storage: WebStorageService) {}

    findAll() {
      return this.http
          .get(environment.api, {
              headers: this.getHeaders()
          })
          .map(res => {
              return res.json()
          })
    }

    getHeaders() {
      let headers = new Headers()
      headers.append('Authorization', this.storage.get("COGNITO_TOKEN"))
      return headers
    }

}

先前的代码示例已经在 Angular 5 中进行了测试。此外,请确保根据自己的 Web 框架采用代码。

要测试它,请返回浏览器。登录表单应该弹出;使用我们之前创建的用户凭据填写字段。然后,单击“登录”按钮:

用户身份将被返回,并且将使用请求头中包含的令牌调用 RESTful API。 API 网关将验证令牌,并将调用FindAllMovies Lambda 函数,该函数将从 DynamoDB 表返回电影:

对于 Web 开发人员,Cognito 的getSession方法可用于从本地存储中检索当前用户,因为 JavaScript SDK 配置为在正确进行身份验证后自动存储令牌,如下面的屏幕截图所示:

总之,到目前为止,我们已经完成了以下工作:

  • 构建了多个 Lambda 函数来管理电影存储

  • 在 DynamoDB 表中管理 Lambda 数据持久性

  • 通过 API Gateway 公开这些 Lambda 函数

  • 在 S3 中构建用于测试构建堆栈的 Web 客户端

  • 通过 CloudFront 分发加速 Web 客户端资产

  • 在 Route 53 中设置自定义域名

  • 使用 AWS Cognito 保护 API

以下模式说明了我们迄今为止构建的无服务器架构:

Amazon Cognito 可以配置多个身份提供者,如 Facebook、Twitter、Google 或开发人员认证的身份。

加密环境变量

在之前的章节中,我们看到如何使用 AWS Lambda 的环境变量动态传递数据到函数代码,而不更改任何代码。根据Twelve Factor App方法论(12factor.net/),您应该始终将配置与代码分开,以避免将敏感凭据检查到存储库,并能够定义 Lambda 函数的多个发布版本(暂存、生产和沙盒)具有相同的源代码。此外,环境变量可用于根据不同设置更改函数行为(A/B 测试)

如果要在多个 Lambda 函数之间共享秘密,可以使用 AWS 的系统管理器参数存储

以下示例说明了如何使用环境变量将 MySQL 凭据传递给函数的代码:

func handler() error {
  MYSQL_USERNAME := os.Getenv("MYSQL_USERNAME")
  MYSQL_PASSWORD := os.Getenv("MYSQL_PASSWORD")
  MYSQL_DATABASE := os.Getenv("MYSQL_DATABASE")
  MYSQL_PORT := os.Getenv("MYSQL_PORT")
  MYSQL_HOST := os.Getenv("MYSQL_HOST")

  uri := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", MYSQL_USERNAME, MYSQL_PASSWORD, MYSQL_HOST, MYSQL_PORT, MYSQL_DATABASE)
  db, err := sql.Open("mysql", uri)
  if err != nil {
    return err
  }
  defer db.Close()

  _, err = db.Query(`CREATE TABLE IF NOT EXISTS movies(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL)`)
  if err != nil {
    return err
  }

  for _, movie := range []string{"Iron Man", "Thor", "Avengers", "Wonder Woman"} {
    _, err := db.Query("INSERT INTO movies(name) VALUES(?)", movie)
    if err != nil {
      return err
    }
  }

  movies, err := db.Query("SELECT id, name FROM movies")
  if err != nil {
    return err
  }

  for movies.Next() {
    var name string
    var id int
    err = movies.Scan(&id, &name)
    if err != nil {
      return err
    }

    log.Printf("ID=%d\tName=%s\n", id, name)
  }
  return nil
}

一旦函数部署到 AWS Lambda 并设置环境变量,就可以调用该函数。它将输出插入到数据库中的电影列表:

到目前为止,一切都很好。但是,数据库凭据是明文!

幸运的是,AWS Lambda 在两个级别提供加密:在传输和静态时,使用 AWS 密钥管理服务。

数据静态加密

AWS Lambda 在部署函数时加密所有环境变量,并在调用函数时解密它们(即时)。

如果展开“加密配置”部分,您会注意到默认情况下,AWS Lambda 使用默认的 Lambda 服务密钥对环境变量进行加密。此密钥在您在特定区域创建 Lambda 函数时会自动创建:

您可以通过导航到身份和访问管理控制台来更改密钥并使用自己的密钥。然后,单击“加密密钥”:

单击“创建密钥”按钮创建新的客户主密钥:

选择一个 IAM 角色和帐户来通过密钥管理服务KMS)API 管理密钥。然后,选择您在创建 Lambda 函数时使用的 IAM 角色。这允许 Lambda 函数使用客户主密钥CMK)并成功请求encryptdecrypt方法:

创建密钥后,返回 Lambda 函数配置页面,并将密钥更改为您刚刚创建的密钥:

现在,当存储在 Amazon 中时,AWS Lambda 将使用您自己的密钥加密环境变量。

数据传输加密

建议在部署函数之前对环境变量(敏感信息)进行加密。AWS Lambda 在控制台上提供了加密助手,使此过程易于遵循。

为了通过在传输中加密(使用之前使用的 KMS),您需要通过选中“启用传输加密的帮助程序”复选框来启用此功能:

通过单击适当的加密按钮对MYSQL_USERNAMEMYSQL_PASSWORD进行加密:

凭据将被加密,并且您将在控制台中看到它们作为CipherText。接下来,您需要更新函数的处理程序,使用 KMS SDK 解密环境变量:

var encryptedMysqlUsername string = os.Getenv("MYSQL_USERNAME")
var encryptedMysqlPassword string = os.Getenv("MYSQL_PASSWORD")
var mysqlDatabase string = os.Getenv("MYSQL_DATABASE")
var mysqlPort string = os.Getenv("MYSQL_PORT")
var mysqlHost string = os.Getenv("MYSQL_HOST")
var decryptedMysqlUsername, decryptedMysqlPassword string

func decrypt(encrypted string) (string, error) {
  kmsClient := kms.New(session.New())
  decodedBytes, err := base64.StdEncoding.DecodeString(encrypted)
  if err != nil {
    return "", err
  }
  input := &kms.DecryptInput{
    CiphertextBlob: decodedBytes,
  }
  response, err := kmsClient.Decrypt(input)
  if err != nil {
    return "", err
  }
  return string(response.Plaintext[:]), nil
}

func init() {
  decryptedMysqlUsername, _ = decrypt(encryptedMysqlUsername)
  decryptedMysqlPassword, _ = decrypt(encryptedMysqlPassword)
}

func handler() error {
  uri := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", decryptedMysqlUsername, decryptedMysqlPassword, mysqlHost, mysqlPort, mysqlDatabase)
  db, err := sql.Open("mysql", uri)
  if err != nil {
    return err
  }
  ...
}

如果您使用自己的 KMS 密钥,您需要授予附加到 Lambda 函数的执行角色(IAM 角色)kms:Decrypt权限。还要确保增加默认执行超时时间,以允许足够的时间完成函数的代码。

使用 CloudTrail 记录 AWS Lambda API 调用

捕获 Lambda 函数发出的所有调用对于审计、安全和合规性非常重要。它为您提供了与其交互的 AWS 服务的全局概览。利用此功能的一个服务是CloudTrail

CloudTrail 记录了 Lambda 函数发出的 API 调用。这很简单易用。您只需要从 AWS 管理控制台导航到 CloudTrail,并按事件源筛选事件,事件源应为lambda.amazonaws.com

在那里,您应该看到每个 Lambda 函数发出的所有调用,如下面的屏幕截图所示:

除了公开事件历史记录,您还可以在每个 AWS 区域中创建一个跟踪,将 Lambda 函数的事件记录在单个 S3 存储桶中,然后使用ELK(Elasticsearch、Logstash 和 Kibana)堆栈实现日志分析管道,如下所示处理您的日志:

最后,您可以创建交互式和动态小部件,构建 Kibana 中的仪表板,以查看 Lambda 函数事件:

为您的依赖项进行漏洞扫描

由于大多数 Lambda 函数代码包含多个第三方 Go 依赖项(记住go get命令),因此对所有这些依赖项进行审计非常重要。因此,漏洞扫描您的 Golang 依赖项应该成为您的 CI/CD 的一部分。您必须使用第三方工具(如S****nyk (snyk.io/)自动化安全分析,以持续扫描依赖项中已知的安全漏洞。以下截图描述了您可能选择为 Lambda 函数实施的完整端到端部署过程:

通过将漏洞扫描纳入工作流程,您将能够发现并修复软件包中已知的漏洞,这些漏洞可能导致数据丢失、服务中断和对敏感信息的未经授权访问。

此外,应用程序最佳实践仍然适用于无服务器架构,如代码审查和 git 分支等软件工程实践,以及安全性安全检查,如输入验证或净化,以避免 SQL 注入。

摘要

在本章中,您学习了一些构建基于 Lambda 函数的安全无服务器应用程序的最佳实践和建议。我们介绍了 Amazon Cognito 如何作为身份验证提供程序,并如何与 API Gateway 集成以保护 API 端点。然后,我们看了 Lambda 函数代码实践,如使用 AWS KMS 加密敏感数据和输入验证。此外,其他实践也可能非常有用和救命,例如应用配额和节流以防止消费者消耗所有 Lambda 函数容量,以及每个函数使用一个 IAM 角色以利用最小特权原则。

在下一章中,我们将讨论 Lambda 定价模型以及如何根据预期负载估算价格。

问题

  1. 将用户池中的用户与身份池集成,以允许用户使用其 Facebook 帐户登录。

  2. 将用户池中的用户与身份池集成,以允许用户使用其 Twitter 帐户登录。

  3. 将用户池中的用户与身份池集成,以允许用户使用其 Google 帐户登录。

  4. 实现一个表单,允许用户在 Web 应用程序上创建帐户,以便他们能够登录。

  5. 为未经身份验证的用户实现忘记密码流程。

第十三章:设计成本效益的应用程序

在本章中,我们将讨论 AWS Lambda 的定价模型,并学习如何根据预期负载估算这个价格。我们还将介绍一些优化和降低无服务器应用成本的技巧,同时保持弹性和可用性。本章将涵盖以下主题:

  • Lambda 定价模型

  • 最佳内存大小

  • 代码优化

  • Lambda 成本和内存跟踪

Lambda 定价模型

AWS Lambda 改变了运维团队配置和管理组织基础设施的方式。客户现在可以在不担心底层基础设施的情况下运行他们的代码,同时支付低廉的价格。每月的前 100 万次请求是免费的,之后每 100 万次请求收费 0.20 美元,因此您可能会无限期地使用 Lambda 的免费套餐。然而,如果您不额外关注函数的资源使用和代码优化,密集的使用情况和大量的工作负载应用可能会不必要地花费您数千美元。

为了控制 Lambda 成本,您必须了解 Lambda 定价模型的工作原理。有三个因素决定了函数的成本:

  • 执行次数:调用次数;每次请求支付 0.0000002 美元。

  • 分配的内存:为函数分配的 RAM 量(范围在 128 MB 和 3,008 MB 之间)。

  • 执行时间:持续时间是从代码开始执行到返回响应或其他终止的时间。时间向最接近的 100 毫秒取整(Lambda 按 100 毫秒的增量计费),并且您可以设置的最大超时时间为 5 分钟。

  • 数据传输:如果您的 Lambda 函数发起外部数据传输,将按照 EC2 数据传输速率收费(aws.amazon.com/ec2/pricing)。

Lambda 成本计算器

现在您已经熟悉了定价模型,让我们看看如何提前计算 Lambda 函数的成本。

在前几章中,我们为FindAllMovies函数分配了 128 MB 的内存,并将执行超时设置为 3 秒。假设函数每秒执行 10 次(一个月内执行 2500 万次)。您的费用将如下计算:

  • 每月计算费用:每月计算价格为每 GB/s 0.00001667 美元,免费套餐提供 400,000 GB/s。总计算(秒)=25 百万(1 秒)=25,000,000 秒。总计算(GB/s)=25,000,000128 MB/1,024=3,125,000 GB/s。

总计算-免费套餐计算=每月应付费计算 GB/s

3,125,000 GB/s - 400,000 免费套餐 GB/s = 2,725,000 GB/s

每月计算费用=2,725,000 GB/s*$0.00001667=$45.42

  • 每月请求费用:每月请求价格为每 100 万次请求 0.20 美元,免费套餐提供每月 100 万次请求。

总请求次数-免费套餐请求=每月应付费请求

25 百万次请求-1 百万免费套餐请求=24 百万次每月应付费请求

每月请求费用=24 百万*$0.2/百万=$4.8

因此,总月费用是计算和请求费用的总和,如下所示:

总费用=计算费用+请求费用=45.24 美元+4.8 美元=50.04 美元

最佳内存大小

正如我们在前一节中看到的,分配的 RAM 数量会影响计费。此外,它还会影响函数接收的 CPU 和网络带宽的数量。因此,您需要选择最佳的内存大小。为了找到函数的价格和性能的正确平衡和最佳水平,您必须使用不同的内存设置测试您的 Lambda 函数,并分析函数实际使用的内存。幸运的是,AWS Lambda 会在关联的日志组中写入日志条目。日志包含每个请求的函数分配和使用的内存量。以下是日志输出的示例:

通过比较内存大小和最大内存使用字段,您可以确定您的函数是否需要更多内存,或者您是否过度配置了函数的内存大小。如果您的函数需要更多内存,您可以随时从“基本设置”部分为其提供更多内存,具体如下:

点击“保存”,然后再次调用函数。在日志输出中,您会注意到内存大小会影响执行时间:

增加函数内存设置将提供显著的性能提升。随着 Lambda 中内存设置的增加,成本将线性增加。同样,减少函数内存设置可能有助于降低成本,但这也会增加执行时间,并且在最坏的情况下可能导致超时或内存超限错误。

将最小内存设置分配给 Lambda 函数并不总是会提供最低总成本。由于内存不足,函数可能会失败和超时。此外,完成所需的时间可能会更长。因此,您将支付更多费用。

代码优化

在前面的部分中,我们看到了如何使用不同的内存设置在规模上测试函数会导致分配更多的 CPU 容量,这可能会影响 Lambda 函数的性能和成本。然而,在优化资源使用之前,您需要先优化函数的代码,以帮助减少需要执行的内存和 CPU 的数量。与传统应用程序相反,AWS Lambda 会为您管理和修补基础架构,这使开发人员可以专注于编写高质量、高效和世界级的代码,以便快速执行。

为函数分配更多资源可能会导致更快的执行,直到达到一定阈值,增加更多内存将不再提供更好的性能。

设计 AWS Lambda 函数时,要考虑以下几点,以便以成本效益的方式进行设计:

  • 对于某些请求,可以使用热容器。有了这些知识,我们可以通过实施以下操作来改善 Lambda 函数的性能:

  • 通过使用全局变量和单例模式,避免在每次调用时重新初始化变量。

  • 保持数据库和 HTTP 连接的活动状态并重复使用,这些连接是在先前的调用期间建立的。在 Go 中,您可以使用 init 函数来设置所需的状态,并在加载函数处理程序时运行一次性计算。

  • 设计您的架构为异步;解耦的组件可能需要更少的计算时间来完成其工作,而不是紧密耦合的组件。此外,避免花费 CPU 周期等待同步请求的响应。

  • 使用监控和调试工具,如 AWS X-Ray,分析和排除性能瓶颈、延迟峰值和其他影响 Lambda 应用性能的问题。

  • 使用并发预留来设置限制,以防止无限自动缩放、冷启动,并保护下游服务。您还可以通过在 Lambda 触发器和函数之间放置 简单队列服务(SQS)来限制执行次数,调整 Lambda 函数触发的频率。

Lambda 成本和内存跟踪

在 AWS Lambda 中设计成本效益的无服务器应用的关键在于监控成本和资源使用情况。不幸的是,CloudWatch 并未提供有关资源使用或 Lambda 函数成本的开箱即用指标。幸运的是,对于每次执行,Lambda 函数都会将执行日志写入 CloudWatch,如下所示:

REPORT RequestId: 147e72f8-5143-11e8-bba3-b5140c3dea53 Duration: 12.00 ms Billed Duration: 100 ms  Memory Size: 128 MB Max Memory Used: 21 MB 

前面的日志显示了给定请求分配和使用的内存。这些值可以通过简单的 CloudWatch 日志指标过滤器提取。此功能使您能够在日志中搜索特定关键字。

打开 AWS CloudWatch 控制台,并从导航窗格中选择“日志组”。接下来,搜索与您的 Lambda 函数关联的日志组。它的名称应该是:/aws/lambda/FUNCTION_NAME

接下来,点击“创建度量过滤器”按钮:

定义一个度量过滤器模式,解析以空格分隔的术语。度量过滤器模式必须指定以逗号分隔的名称字段,并用方括号括起整个模式,例如[a,b,c]。然后,点击“测试模式”以测试您的过滤器模式对日志中现有数据的结果。将打印以下记录:

如果您不知道自己有多少字段,可以使用方括号括起来的省略号:

$13将存储分配给函数的内存,$18表示实际使用的内存。接下来,点击“分配度量”以创建已分配内存的度量:

点击“创建过滤器”按钮保存。您现在应该看到新创建的过滤器:

应用相同的步骤为内存使用创建另一个过滤器:

一旦定义了两个过滤器,请确保您的 Lambda 函数正在运行,并在函数填充新的 CloudWatch 指标值时等待几秒钟:

回到 CloudWatch,在我们之前创建的两个度量标准的基础上创建一个新的图表:

您还可以进一步进行,并创建一个几乎实时的 CloudWatch 警报,如果内存使用量超过某个阈值(例如,相对于您分配的内存的 80%)。此外,重要的是要关注函数的持续时间。您可以按照本节中描述的相同过程从 Lambda 执行日志中提取计费持续时间,并根据提取的值设置警报,以便在函数完成所需时间可疑地长时收到通知。

摘要

使用 AWS Lambda 非常简单-您不必预配和管理任何基础设施,并且在几秒钟内就可以轻松运行一些有用的东西。此外,AWS Lambda 相对于 EC2 的一个巨大优势是您不必为闲置资源付费。这非常强大,但也是 Lambda 最大的风险之一。在开发过程中忘记成本是非常常见的,但一旦您开始在生产中运行大量工作负载和多个函数,成本可能会很高。因此,在这成为问题之前,跟踪 Lambda 成本和使用情况非常重要。

最后一章将介绍基础设施即代码(IaC)的概念,以帮助您以自动化的方式设计和部署 N 层无服务器应用程序,以避免人为错误和可重复的任务。

第十四章:基础设施即代码

典型的基于 Lambda 的应用程序由多个函数组成,这些函数由事件触发,例如 S3 存储桶中的新对象,传入的 HTTP 请求或新的 SQS 消息。这些函数可以独立存在,也可以利用其他资源,例如 DynamoDB 表,Amazon S3 存储桶和其他 Lambda 函数。到目前为止,我们已经看到如何从 AWS 管理控制台或使用 AWS CLI 创建这些资源。在实际情况下,您希望花费更少的时间来提供所需的资源,并更多地专注于应用程序逻辑。最终,这就是无服务器的方法。

这最后一章将介绍基础设施即代码的概念,以帮助您以自动化的方式设计和部署 N-Tier 无服务器应用程序,以避免人为错误和可重复的任务。

技术要求

本书假设您对 AWS 无服务器应用程序模型有一些基本了解。如果您对 SAM 本身还不熟悉,请参阅第一章,无服务器 Go,直到第十章,测试您的无服务器应用程序。您将获得一个逐步指南,了解如何开始使用 SAM。本章的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-serverless-Applications-with-Go

使用 Terraform 部署 AWS Lambda

Terraform是 HashiCorp 构建的开源自动化工具。它用于通过声明性配置文件创建,管理和更新基础设施资源。它支持以下提供程序:

  • 云提供商:AWS,Azure,Oracle Cloud 和 GCP

  • 基础设施软件

  • Consul:这是一个分布式,高可用的服务发现和配置系统。

  • Docker:这是一个旨在通过使用容器更轻松地创建,部署和运行应用程序的工具。

  • Nomad:这是一个易于使用的企业级集群调度程序。

  • Vault:这是一个提供安全,可靠的存储和分发机密的工具。

  • 其他SaaSPaaS

Terraform 不是配置管理工具(如 Ansible,Chef 和 Puppet&Salt)。它是用来生成和销毁基础设施的,而配置管理工具用于在现有基础设施上安装东西。但是,Terraform 可以进行一些配置(www.terraform.io/docs/provisioners/index.html)。

这个指南将向您展示如何使用 Terraform 部署 AWS Lambda,因此您需要安装 Terraform。您可以找到适合您系统的包并下载它(www.terraform.io/downloads.html)。下载后,请确保terraform二进制文件在PATH变量中可用。配置您的凭据,以便 Terraform 能够代表您进行操作。以下是提供身份验证凭据的四种方法:

  • 通过提供商直接提供 AWS access_keysecret_key

  • AWS 环境变量。

  • 共享凭据文件。

  • EC2 IAM 角色。

如果您遵循了第二章,开始使用 AWS Lambda,您应该已经安装并配置了 AWS CLI。因此,您无需采取任何行动。

创建 Lambda 函数

要开始创建 Lambda 函数,请按照以下步骤进行:

  1. 使用以下结构创建一个新项目:

  1. 我们将使用最简单的 Hello world 示例。function文件夹包含一个基于 Go 的 Lambda 函数,显示一个简单的消息:
package main

import "github.com/aws/aws-lambda-go/lambda"

func handler() (string, error) {
  return "First Lambda function with Terraform", nil
}
func main() {
  lambda.Start(handler)
}
  1. 您可以构建基于 Linux 的二进制文件,并使用以下命令生成deployment包:
GOOS=linux go build -o main main.go
zip deployment.zip main
  1. 现在,函数代码已经定义,让我们使用 Terraform 创建我们的第一个 Lambda 函数。将以下内容复制到main.tf文件中:
provider "aws" {
  region = "us-east-1"
}

resource "aws_iam_role" "role" {
  name = "PushCloudWatchLogsRole"
  assume_role_policy = "${file("assume-role-policy.json")}"
}

resource "aws_iam_policy" "policy" {
  name = "PushCloudWatchLogsPolicy"
  policy = "${file("policy.json")}"
}

resource "aws_iam_policy_attachment" "profile" {
  name = "cloudwatch-lambda-attachment"
  roles = ["${aws_iam_role.role.name}"]
  policy_arn = "${aws_iam_policy.policy.arn}"
}

resource "aws_lambda_function" "demo" {
  filename = "function/deployment.zip"
  function_name = "HelloWorld"
  role = "${aws_iam_role.role.arn}"
  handler = "main"
  runtime = "go1.x"
}
  1. 这告诉 Terraform 我们将使用 AWS 提供程序,并默认为创建我们的资源使用us-east-1区域:
  • IAM 角色是在执行期间 Lambda 函数将要承担的执行角色。它定义了我们的 Lambda 函数可以访问的资源:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
  • IAM 策略是授予我们的 Lambda 函数权限的权限列表,以将其日志流式传输到 CloudWatch。以下策略将附加到 IAM 角色:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "1",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:CreateLogGroup",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
  • Lambda 函数是一个基于 Go 的 Lambda 函数。部署包可以直接指定为本地文件(使用filename属性)或通过 Amazon S3 存储桶。有关如何将 Lambda 函数部署到 AWS 的详细信息,请参阅第六章,部署您的无服务器应用
  1. 在终端上运行terraform init命令以下载和安装 AWS 提供程序,如下所示:

  1. 使用terraform plan命令创建执行计划(模拟运行)。它会提前显示将要创建的内容,这对于调试和确保您没有做错任何事情非常有用,如下一个屏幕截图所示:

  1. 在将其部署到 AWS 之前,您将能够检查 Terraform 的执行计划。准备好后,通过发出以下命令应用更改:
terraform apply
  1. 确认配置,输入yes。将显示以下输出(为简洁起见,某些部分已被裁剪):

确保用于执行这些命令的 IAM 用户具有执行 IAM 和 Lambda 操作的权限。

  1. 如果返回 AWS Lambda 控制台,应该创建一个新的 Lambda 函数。如果尝试调用它,应返回预期的消息,如下一个屏幕截图所示:

  1. 到目前为止,我们在模板文件中定义了 AWS 区域和函数名称。但是,我们使用基础架构即代码工具的原因之一是可用性和自动化。因此,您应始终使用变量并避免硬编码值。幸运的是,Terraform 允许您定义自己的变量。为此,请创建一个variables.tf文件,如下所示:
variable "aws_region" {
  default = "us-east-1"
  description = "AWS region"
}

variable "lambda_function_name" {
  default = "DemoFunction"
  description = "Lambda function's name"
}
  1. 更新main.tf以使用变量而不是硬编码的值。注意使用${var.variable_name}关键字:
provider "aws" {
  region = "${var.aws_region}"
}

resource "aws_lambda_function" "demo" {
  filename = "function/deployment.zip"
  function_name = "${var.lambda_function_name}"
  role = "${aws_iam_role.role.arn}"
  handler = "main"
  runtime = "go1.x"
}
  1. 函数按预期工作后,使用 Terraform 创建我们迄今为止构建的无服务器 API。

  2. 在一个新目录中,创建一个名为main.tf的文件,其中包含以下配置:

resource "aws_iam_role" "role" {
 name = "FindAllMoviesRole"
 assume_role_policy = "${file("assume-role-policy.json")}"
}

resource "aws_iam_policy" "cloudwatch_policy" {
 name = "PushCloudWatchLogsPolicy"
 policy = "${file("cloudwatch-policy.json")}"
}

resource "aws_iam_policy" "dynamodb_policy" {
 name = "ScanDynamoDBPolicy"
 policy = "${file("dynamodb-policy.json")}"
}

resource "aws_iam_policy_attachment" "cloudwatch-attachment" {
 name = "cloudwatch-lambda-attchment"
 roles = ["${aws_iam_role.role.name}"]
 policy_arn = "${aws_iam_policy.cloudwatch_policy.arn}"
}

resource "aws_iam_policy_attachment" "dynamodb-attachment" {
 name = "dynamodb-lambda-attchment"
 roles = ["${aws_iam_role.role.name}"]
 policy_arn = "${aws_iam_policy.dynamodb_policy.arn}"
}
  1. 上述代码片段创建了一个具有扫描 DynamoDB 表和将日志条目写入 CloudWatch 权限的 IAM 角色。使用 DynamoDB 表名作为环境变量配置一个基于 Go 的 Lambda 函数:
resource "aws_lambda_function" "findall" {
  function_name = "FindAllMovies"
  handler = "main"
  filename = "function/deployment.zip"
  runtime = "go1.x"
  role = "${aws_iam_role.role.arn}"

  environment {
    variables {
      TABLE_NAME = "movies"
    }
  }
}

设置 DynamoDB 表

接下来,我们必须设置 DynamoDB 表。执行以下步骤:

  1. 为表的分区键创建一个 DynamoDB 表:
resource "aws_dynamodb_table" "movies" {
  name = "movies"
  read_capacity = 5
  write_capacity = 5
  hash_key = "ID"

  attribute {
      name = "ID"
      type = "S"
  }
}
  1. 使用新项目初始化movies表:
resource "aws_dynamodb_table_item" "items" {
  table_name = "${aws_dynamodb_table.movies.name}"
  hash_key = "${aws_dynamodb_table.movies.hash_key}"
  item = "${file("movie.json")}"
}
  1. 项目属性在movie.json文件中定义:
{
  "ID": {"S": "1"},
  "Name": {"S": "Ant-Man and the Wasp"},
  "Description": {"S": "A Marvel's movie"},
  "Cover": {"S": http://COVER_URL.jpg"}
}

配置 API Gateway

最后,我们需要通过 API Gateway 触发函数:

  1. 在 REST API 上创建一个movies资源,并在其上公开一个GET方法。如果传入的请求与定义的资源匹配,它将调用之前定义的 Lambda 函数:
resource "aws_api_gateway_rest_api" "api" {
  name = "MoviesAPI"
}

resource "aws_api_gateway_resource" "proxy" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  parent_id = "${aws_api_gateway_rest_api.api.root_resource_id}"
  path_part = "movies"
}

resource "aws_api_gateway_method" "proxy" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  resource_id = "${aws_api_gateway_resource.proxy.id}"
  http_method = "GET"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  resource_id = "${aws_api_gateway_method.proxy.resource_id}"
  http_method = "${aws_api_gateway_method.proxy.http_method}"

  integration_http_method = "POST"
  type = "AWS_PROXY"
  uri = "${aws_lambda_function.findall.invoke_arn}"
}
  1. 发出以下命令安装 AWS 插件,生成执行计划并应用更改:
terraform init
terraform plan
terraform apply
  1. 创建整个基础架构应该只需要几秒钟。创建步骤完成后,Lambda 函数应该已创建并正确配置,如下一个屏幕截图所示:

  1. API Gateway 也是一样,应该定义一个新的 REST API,其中/movies资源上有一个GET方法,如下所示:

  1. 在 DynamoDB 控制台中,应创建一个新表,并在下一个屏幕截图中显示一个电影项目:

  1. 为了调用我们的 API Gateway,我们需要部署它。创建一个部署阶段,让我们称之为staging
resource "aws_api_gateway_deployment" "staging" {
  depends_on = ["aws_api_gateway_integration.lambda"]

  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  stage_name = "staging"
}
  1. 我们将使用 Terraform 的输出功能来公开 API URL;创建一个outputs.tf文件,内容如下:
output "API Invocation URL" {
  value = "${aws_api_gateway_deployment.staging.invoke_url}"
}
  1. 再次运行terraform apply以创建这些新对象,它将检测到更改并要求您确认它应该执行的操作,如下所示:

  1. API Gateway URL 将显示在输出部分;将其复制到剪贴板:

  1. 如果您将您喜欢的浏览器指向 API 调用 URL,将显示错误消息,如下一张截图所示:

  1. 我们将通过授予 API Gateway 调用 Lambda 函数的执行权限来解决这个问题。更新main.tf文件以创建aws_lambda_permission资源:
resource "aws_lambda_permission" "apigw" {
  statement_id = "AllowAPIGatewayInvoke"
  action = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.findall.arn}"
  principal = "apigateway.amazonaws.com"

  source_arn = "${aws_api_gateway_deployment.staging.execution_arn}/*/*"
}
  1. 使用terraform apply命令应用最新更改。在 Lambda 控制台上,API Gateway 触发器应该显示如下:

  1. 在您喜欢的网络浏览器中加载输出中给出的 URL。如果一切正常,您将以 JSON 格式在 DynamoDB 表中看到存储的电影,如下一张截图所示:

Terraform 将基础设施的状态存储在状态文件(.tfstate)中。状态包含资源 ID 和所有资源属性。如果您使用 Terraform 创建 RDS 实例,则数据库凭据将以明文形式存储在状态文件中。因此,您应该将文件保存在远程后端,例如 S3 存储桶中。

清理

最后,要删除所有资源(Lambda 函数、IAM 角色、IAM 策略、DynamoDB 表和 API Gateway),您可以发出terraform destroy命令,如下所示:

如果您想删除特定资源,可以使用--target选项,如下所示:terraform destroy --target=RESOURCE_NAME。操作将仅限于资源及其依赖项。

到目前为止,我们已经使用模板文件定义了 AWS Lambda 函数及其依赖关系。因此,我们可以像任何其他代码一样对其进行版本控制。我们使用和配置的整个无服务器基础设施被视为源代码,使我们能够在团队成员之间共享它,在其他 AWS 区域中复制它,并在失败时回滚。

使用 CloudFormation 部署 AWS Lambda

AWS CloudFormation是一种基础设施即代码工具,用于以声明方式指定资源。您可以在蓝图文档(模板)中对您希望 AWS 启动的所有资源进行建模,AWS 会为您创建定义的资源。因此,您花费更少的时间管理这些资源,更多的时间专注于在 AWS 中运行的应用程序。

Terraform 几乎涵盖了 AWS 的所有服务和功能,并支持第三方提供商(平台无关),而 CloudFormation 是 AWS 特定的(供应商锁定)。

您可以使用 AWS CloudFormation 来指定、部署和配置无服务器应用程序。您创建一个描述无服务器应用程序依赖关系的模板(Lambda 函数、DynamoDB 表、API Gateway、IAM 角色等),AWS CloudFormation 负责为您提供和配置这些资源。您不需要单独创建和配置 AWS 资源,并弄清楚什么依赖于什么。

在我们深入了解 CloudFormation 之前,我们需要了解模板结构:

  • AWSTemplateFormatVersion:CloudFormation 模板版本。

  • Description:模板的简要描述。

  • Mappings:键和相关值的映射,可用于指定条件参数值。

  • Parameters:运行时传递给模板的值。

  • Resources:AWS 资源及其属性(Lambda、DynamoDB、S3 等)。

  • 输出:描述每当查看堆栈属性时返回的值。

了解 AWS CloudFormation 模板的不同部分后,您可以将它们放在一起,并在template.yml文件中定义一个最小模板,如下所示:

AWSTemplateFormatVersion: "2010-09-09"
Description: "Simple Lambda Function"
Parameters:
  FunctionName:
    Description: "Function name"
    Type: "String"
    Default: "HelloWorld"
  BucketName:
    Description: "S3 Bucket name"
    Type: "String"
Resources:
  ExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - PolicyName: "PushCloudWatchLogsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
              - Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
              - Resource: "*"
  HelloWorldFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        S3Bucket: !Ref BucketName
        S3Key: deployment.zip
      FunctionName: !Ref FunctionName
      Handler: "main"
      Runtime: "go1.x"
      Role: !GetAtt ExecutionRole.Arn

上述文件定义了两个资源:

  • ExecutionRole:分配给 Lambda 函数的 IAM 角色,它定义了 Lambda 运行时调用的代码的权限。

  • HelloWorldFunction:AWS Lambda 定义,我们已将运行时属性设置为使用 Go,并将函数的代码存储在 S3 上的 ZIP 文件中。该函数使用 CloudFormation 的内置GetAtt函数引用 IAM 角色;它还使用Ref关键字引用参数部分中定义的变量。

也可以使用 JSON 格式;在 GitHub 存储库中可以找到 JSON 版本(github.com/PacktPublishing/Hands-On-serverless-Applications-with-Go)。

执行以下步骤开始:

  1. 使用以下命令构建后,创建一个 S3 存储桶来存储部署包:
aws s3 mb s3://hands-on-serverless-go-packt/
GOOS=linux go build -o main main.go
zip deployment.zip main
aws s3 cp deployment.zip s3://hands-on-serverless-go-packt/
  1. 转到 AWS CloudFormation 控制台,然后选择“创建堆栈”,如下一个屏幕截图所示:

  1. 在“选择模板”页面上,选择模板文件,它将上传到 Amazon S3 存储桶,如下所示:

  1. 单击“下一步”,定义堆栈名称,并根据需要覆盖默认参数,如下一个屏幕截图所示:

  1. 单击“下一步”,将选项保留为默认值,然后单击“创建”,如下一个屏幕截图所示:

  1. 堆栈将开始创建模板文件中定义的所有资源。创建后,堆栈状态将从CREATE_IN_PROGRESS更改为CREATE_COMPLETE(如果出现问题,将自动执行回滚),如下所示:

  1. 因此,我们的 Lambda 函数应该如下屏幕截图所示创建:

  1. 您始终可以更新您的 CloudFormation 模板文件。例如,让我们创建一个新的 DynamoDB 表:
AWSTemplateFormatVersion: "2010-09-09"
Description: "Simple Lambda Function"
Parameters:
  FunctionName:
    Description: "Function name"
    Type: "String"
    Default: "HelloWorld"
  BucketName:
    Description: "S3 Bucket name"
    Type: "String"
  TableName:
    Description: "DynamoDB Table Name"
    Type: "String"
    Default: "movies"
Resources:
  ExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - 
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - 
          PolicyName: "PushCloudWatchLogsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: "*"
        - 
          PolicyName: "ScanDynamoDBTablePolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - dynamodb:Scan
                Resource: "*"
  HelloWorldFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        S3Bucket: !Ref BucketName
        S3Key: deployment.zip
      FunctionName: !Ref FunctionName
      Handler: "main"
      Runtime: "go1.x"
      Role: !GetAtt ExecutionRole.Arn
      Environment:
        Variables:
          TABLE_NAME: !Ref TableName
  DynamoDBTable:
    Type: "AWS::DynamoDB::Table"
    Properties:
      TableName: !Ref TableName
      AttributeDefinitions:
        -
          AttributeName: "ID"
          AttributeType: "S"
      KeySchema:
        -
          AttributeName: "ID"
          KeyType: "HASH"
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
  1. 在 CloudFormation 控制台上,选择我们之前创建的堆栈,然后从菜单中单击“更新堆栈”,如下所示:

  1. 上传更新后的模板文件,如下所示:

  1. 与 Terraform 类似,AWS CloudFormation 将检测更改并提前显示将更改的资源,如下所示:

  1. 单击“更新”按钮以应用更改。堆栈状态将更改为 UPDATE_IN_PROGRESS,如下一个屏幕截图所示:

  1. 应用更改后,将创建一个新的 DynamoDB 表,并向 Lambda 函数授予 DynamoDB 权限,如下所示:

每当 CloudFormation 必须定义 IAM 角色、策略或相关资源时,--capabilities CAPABILITY_IAM选项是必需的。

  1. AWS CLI 也可以用来使用以下命令创建您的 CloudFormation 堆栈:
aws cloudformation create-stack --stack-name=SimpleLambdaFunction \
 --template-body=file://template.yml \
 --capabilities CAPABILITY_IAM \
 --parameters ParameterKey=BucketName,ParameterValue=hands-on-serverless-go-packt 
 ParameterKey=FunctionName,ParameterValue=HelloWorld \
 ParameterKey=TableName,ParameterValue=movies

CloudFormation 设计师

除了从头开始编写自己的模板外,还可以使用 CloudFormation 设计模板功能轻松创建您的堆栈。以下屏幕截图显示了如何查看到目前为止创建的堆栈的设计:

如果一切顺利,您应该看到以下组件:

现在,您可以通过从左侧菜单拖放组件来创建复杂的 CloudFormation 模板。

使用 SAM 部署 AWS Lambda

AWS 无服务器应用程序模型AWS SAM)是定义无服务器应用程序的模型。AWS SAM 受到 AWS CloudFormation 的本地支持,并定义了一种简化的语法来表达无服务器资源。您只需在模板文件中定义应用程序中所需的资源,并使用 SAM 部署命令创建一个 CloudFormation 堆栈。

之前,我们看到了如何使用 AWS SAM 来本地测试 Lambda 函数。此外,SAM 还可以用于设计和部署函数到 AWS Lambda。您可以使用以下命令初始化一个快速的基于 Go 的无服务器项目(样板):

sam init --name api --runtime go1.x

上述命令将创建一个具有以下结构的文件夹:

sam init命令提供了一种快速创建无服务器应用程序的方法。它生成一个简单的带有关联单元测试的 Go Lambda 函数。此外,将生成一个包含构建和生成部署包步骤列表的 Makefile。最后,将创建一个模板文件,称为 SAM 文件,其中描述了部署函数到 AWS Lambda 所需的所有 AWS 资源。

现在我们知道了如何使用 SAM 生成样板,让我们从头开始编写自己的模板。创建一个名为findall的文件夹,在其中创建一个main.go文件,其中包含FindAllMovies函数的代码内容:

// Movie entity
type Movie struct {
  ID string `json:"id"`
  Name string `json:"name"`
  Cover string `json:"cover"`
  Description string `json:"description"`
}

func findAll() (events.APIGatewayProxyResponse, error) {
  ...
  svc := dynamodb.New(cfg)
  req := svc.ScanRequest(&dynamodb.ScanInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
  })
  res, err := req.Send()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while scanning DynamoDB",
    }, nil
  }

  movies := make([]Movie, 0)
  for _, item := range res.Items {
    movies = append(movies, Movie{
      ID: *item["ID"].S,
      Name: *item["Name"].S,
      Cover: *item["Cover"].S,
      Description: *item["Description"].S,
    })
  }
  ...
  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    },
    Body: string(response),
  }, nil
}

func main() {
  lambda.Start(findAll)
}

接下来,在template.yaml文件中创建一个无服务器应用程序定义。以下示例说明了如何创建一个带有 DynamoDB 表的 Lambda 函数:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::serverless-2016-10-31
Resources:
  FindAllFunction:
    Type: AWS::serverless::Function
    Properties:
      Handler: main
      Runtime: go1.x
      Policies: AmazonDynamoDBFullAccess 
      Environment:
        Variables: 
          TABLE_NAME: !Ref MoviesTable
  MoviesTable: 
     Type: AWS::serverless::SimpleTable
     Properties:
       PrimaryKey:
         Name: ID
         Type: String
       ProvisionedThroughput:
         ReadCapacityUnits: 5
         WriteCapacityUnits: 5

该模板类似于我们之前编写的 CloudFormation 模板。SAM 扩展了 CloudFormation 并简化了表达无服务器资源的语法。

使用package命令将部署包上传到CloudFormation部分中创建的 S3 存储桶:

sam package --template-file template.yaml --output-template-file serverless.yaml \
    --s3-bucket hands-on-serverless-go-packt

上述命令将部署页面上传到 S3 存储桶,如下截图所示:

此外,将基于您提供的定义文件生成一个名为serverless.yaml的 SAM 模板文件。它应该包含指向您指定的 Amazon S3 存储桶中的deployment ZIP 的CodeUri属性:

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  FindAllFunction:
    Properties:
      CodeUri: s3://hands-on-serverless-go-packt/764cf76832f79ca7f29c6397fe7ccd91
      Environment:
        Variables:
          TABLE_NAME:
            Ref: MoviesTable
      Handler: main
      Policies: AmazonDynamoDBFullAccess
      Runtime: go1.x
    Type: AWS::serverless::Function
  MoviesTable:
    Properties:
      PrimaryKey:
        Name: ID
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
    Type: AWS::serverless::SimpleTable
Transform: AWS::serverless-2016-10-31

最后,使用以下命令将函数部署到 AWS Lambda:

sam deploy --template-file serverless.yaml --stack-name APIStack \
 --capabilities CAPABILITY_IAM

CAPABILITY_IAM用于明确确认 AWS CloudFormation 被允许代表您为 Lambda 函数创建 IAM 角色。

当您运行sam deploy命令时,它将创建一个名为 APIStack 的 AWS CloudFormation 堆栈,如下截图所示:

资源创建后,函数应该部署到 AWS Lambda,如下所示:

SAM 范围仅限于无服务器资源(支持的 AWS 服务列表可在以下网址找到:docs.aws.amazon.com/serverlessrepo/latest/devguide/using-aws-sam.html)。

导出无服务器应用程序

AWS Lambda 允许您为现有函数导出 SAM 模板文件。选择目标函数,然后从操作菜单中单击“导出函数”,如下所示:

单击“下载 AWS SAM 文件”以下载模板文件,如下所示:

模板将包含函数的定义、必要的权限和触发器:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::serverless-2016-10-31'
Description: An AWS serverless Specification template describing your function.
Resources:
  FindAllMovies:
    Type: 'AWS::serverless::Function'
    Properties:
      Handler: main
      Runtime: go1.x
      CodeUri: .
      Description: ''
      MemorySize: 128
      Timeout: 3
      Role: 'arn:aws:iam::ACCOUNT_ID:role/FindAllMoviesRole'
      Events:
        Api1:
          Type: Api
          Properties:
            Path: /MyResource
            Method: ANY
        Api2:
          Type: Api
          Properties:
            Path: /movies
            Method: GET
      Environment:
        Variables:
          TABLE_NAME: movies
      Tracing: Active
      ReservedConcurrentExecutions: 10

现在,您可以使用sam packagesam deploy命令将函数导入到不同的 AWS 区域或 AWS 账户中。

总结

管理无服务器应用程序资源可以是非常手动的,或者您可以自动化工作流程。但是,如果您有一个复杂的基础架构,自动化流程可能会很棘手。这就是 AWS CloudFormation、SAM 和 Terraform 等工具发挥作用的地方。

在本章中,我们学习了如何使用基础设施即代码工具来自动化创建 AWS 中无服务器应用程序资源和依赖关系。我们看到了一些特定于云的工具,以及松散耦合的工具,可以在多个平台上运行。然后,我们看到了这些工具如何用于部署基于 Lambda 的应用程序到 AWS。

到目前为止,您可以编写一次无服务器基础设施代码,然后多次使用它。定义基础设施的代码可以进行版本控制、分叉、回滚(回到过去)并用于审计基础设施更改,就像任何其他代码一样。此外,它可以以编程方式发现和解决。换句话说,如果基础设施已经被手动修改,您可以销毁该基础设施并重新生成一个干净的副本——不可变基础设施。

问题

  1. 编写一个 Terraform 模板来创建InsertMovie Lambda 函数资源。

  2. 更新 CloudFormation 模板,以便在收到传入的 HTTP 请求时通过 API Gateway 触发定义的 Lambda 函数。

  3. 编写一个 SAM 文件来建模和定义构建本书中一直使用的无服务器 API 所需的所有资源。

  4. 配置 Terraform 以将生成的状态文件存储在远程 S3 后端。

  5. 为我们在本书中构建的无服务器 API 创建一个 CloudFormation 模板。

  6. 为我们在本书中构建的无服务器 API 创建一个 Terraform 模板。

第十五章:评估

第一章:无服务器

  1. 使用无服务器方法的优势是什么?

答案

    • NoOps:没有管理或配置开销,上市时间更快。
  • 自动缩放和 HA:根据负载增强的可伸缩性和弹性。

  • 成本优化:只为您消耗的计算时间付费。

  • Polygot:利用纳米服务架构的力量。

  1. Lambda 是一种节省时间的方法的原因是什么?

答案:您按执行次数付费,不会为闲置资源付费,而使用 EC2 实例时,您还会为未使用的资源付费。

  1. 无服务器架构如何实现微服务?

答案:微服务是将单片应用程序分解为一组较小和模块化服务的方法。无服务器计算是微服务应用程序的关键启用。它使基础设施变得事件驱动,并完全由构成应用程序的每个服务的需求控制。此外,无服务器意味着函数,而微服务是一组函数。

  1. AWS Lambda 函数的最长时间限制是多少?

答案:默认情况下,每个 Lambda 函数的超时时间为 3 秒;您可以设置的最长持续时间为 5 分钟。

  1. 以下哪些是 AWS Lambda 支持的事件源?
  • 亚马逊 Kinesis 数据流

  • 亚马逊 RDS

  • AWS CodeCommit

  • AWS 云形成

答案:亚马逊 Kinesis 数据流、AWS CodeCommit 和 CloudFormation 是 AWS Lambda 支持的事件源。所有支持的事件源列表可以在以下网址找到:docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html

  1. 解释 Go 中的 goroutine 是什么。如何停止 goroutines?

答案:goroutine 是轻量级线程;它使用一种称为通道的资源进行通信。通道通过设计,防止了在使用 goroutines 访问共享内存时发生竞态条件。要停止 goroutine,我们传递信号通道。该信号通道用于推送一个值。goroutine 定期轮询该通道。一旦检测到信号,它就会退出。

  1. AWS 中的 Lambda@Edge 是什么?

答案:Lambda@Edge 允许您在 CloudFront 的边缘位置运行 Lambda 函数,以便自定义返回给最终用户的内容,延迟最低。

  1. 功能即服务和平台即服务之间有什么区别?

答案:PaaS 和 FaaS 都允许您轻松部署应用程序并在不担心基础架构的情况下进行扩展。但是,FaaS 可以节省您的资金,因为您只需为处理传入请求所使用的计算时间付费。

  1. 什么是 AWS Lambda 冷启动?

答案:当触发新事件时会发生冷启动;AWS Lambda 创建和初始化一个新实例或容器来处理请求,这比热启动需要更长的时间(启动延迟),在热启动中,容器是从先前的事件中重用的。

  1. AWS Lambda 函数可以是无状态的还是有状态的?

答案:Lambda 函数必须是无状态的,以利用由于传入事件速率增加而导致的自动扩展的能力。

第二章:开始使用 AWS Lambda

  1. AWS CLI 不支持哪种格式?
  • JSON

  • XML

  • 文本

答案:支持的值为 JSON、表和文本。默认输出为 JSON。

  1. 是否建议使用 AWS 根帐户进行日常与 AWS 的交互?如果是的话,为什么?

答案:AWS 根帐户具有创建和删除 AWS 资源、更改计费甚至关闭 AWS 帐户的最终权限。因此,强烈建议为日常任务创建一个仅具有所需权限的 IAM 用户。

  1. 您需要设置哪些环境变量才能使用 AWS CLI?

答案:以下是配置 AWS CLI 所需的环境变量:

    • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY

  • AWS_DEFAULT_REGION

  1. 如何使用具有命名配置文件的 AWS CLI?

回答AWS_PROFILE可用于设置要使用的 CLI 配置文件。配置文件存储在凭据文件中。默认情况下,AWS CLI 使用default配置文件。

  1. 解释 GOPATH 环境变量。

回答GOPATH环境变量指定 Go 工作区的位置。默认值为$HOME/go

  1. 哪个命令行命令编译 Go 程序?
  • go build

  • go run

  • go fmt

  • go doc

回答:上述命令执行以下操作:

    • build:它是一个编译包和依赖项并生成单个二进制文件。
  • run:它是一个编译和运行 Go 程序。

  • fmt:它是一个重新格式化包资源。

  • doc:它是一个显示包或函数文档的包。

  1. 什么是 Go 工作区?

回答:Go 工作区是一个您将加载和处理 Go 代码的目录。该目录必须具有以下层次结构:

    • src:它包含 Go 源文件。
  • bin:它包含可执行文件。

  • pkg:它包含包对象。

第三章:使用 Lambda 开发无服务器函数

  1. 创建 AWS Lambda 函数的 IAM 角色的命令行命令是什么?

回答:使用以下命令创建一个 IAM 角色;它允许 Lambda 函数调用您帐户下的 AWS 服务:

aws iam create-role ROLE_NAME --assume-role-policy-document file://assume-role-lambda.json

assume-role-lambda.json文件包含以下内容:

{  
 "Version":"2012-10-17",
 "Statement":[  
  {  
  "Effect":"Allow",
  "Principal":{  
   "AWS":"*"
  },
  "Action":"sts:AssumeRole"
  }
 ]
} 
  1. 在弗吉尼亚地区(us-east-1)创建一个新的 S3 存储桶并将 Lambda 部署包上传到其中的命令行命令是什么?

回答:以下命令可用于创建一个 S3 存储桶:

aws s3 mb s3://BUCKET_NAME --region us-east-1

要将部署包上传到存储桶,发出以下命令:

aws s3 cp deployment.zip s3://BUCKET_NAME --region us-east-1
  1. Lambda 包大小限制是多少?
  • 10 MB

  • 50 MB

  • 250 MB

回答:AWS Lambda 部署包的总最大限制为 50MB 压缩和 250MB 未压缩。

  1. AWS Lambda 控制台支持编辑 Go 源代码。

回答:错误;Go 是最近添加的语言,其开发人员尚未添加内联编辑器的功能。因此,您必须提供一个 ZIP 文件格式的可执行二进制文件或引用一个 S3 存储桶和对象键,您已经上传了部署包。

  1. AWS Lambda 执行环境的基础是什么?
  • 亚马逊 Linux 镜像

  • 微软 Windows 服务器

回答:AWS Lambda 执行环境基于亚马逊 Linux AMI。

  1. AWS Lambda 中如何表示事件?

回答:AWS Lambda 中的事件以 JSON 格式表示。

第五章:使用 DynamoDB 管理数据持久性

  1. 实现更新处理程序以更新现有的电影项目。

回答:处理程序期望以 JSON 格式的电影项目;输入将被编码为Movie结构。使用PutItem方法将电影插入表中,如下所示:

func update(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  var movie Movie
  err := json.Unmarshal([]byte(request.Body), &movie)
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: 400,
      Body: "Invalid payload",
    }, nil
  }

  ...

  svc := dynamodb.New(cfg)
  req := svc.PutItemRequest(&dynamodb.PutItemInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
    Item: map[string]dynamodb.AttributeValue{
      "ID": dynamodb.AttributeValue{
        S: aws.String(movie.ID),
      },
      "Name": dynamodb.AttributeValue{
        S: aws.String(movie.Name),
      },
    },
  })
  _, err = req.Send()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while updating the movie",
    }, nil
  }

  response, err := json.Marshal(movie)
  ...

  return events.APIGatewayProxyResponse{
    StatusCode: 200,
    Body: string(response),
    Headers: map[string]string{
      "Content-Type": "application/json",
    },
  }, nil
}

  1. 在 API Gateway 中创建一个新的 PUT 方法来触发update Lambda 函数。

回答:在/movies资源上公开一个PUT方法,并配置目标为之前定义的 Lambda 函数。以下截图展示了结果:

  1. 实现一个单一的 Lambda 函数来处理所有类型的事件(GET、POST、DELETE、PUT)。

回答

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
 switch request.HTTPMethod {
 case http.MethodGet:
 // get all movies handler
 break
 case http.MethodPost:
 // insert movie handler
 break
 case http.MethodDelete:
 // delete movie handler
 break
 case http.MethodPut:
 // update movie handler
 break
 default:
 return events.APIGatewayProxyResponse{
 StatusCode: http.StatusMethodNotAllowed,
 Body: "Unsupported HTTP method",
 }, nil
 }
}
  1. 更新findOne处理程序以返回对于有效请求但空数据(例如,所请求的 ID 没有电影)的适当响应代码。

回答:在处理用户输入(在我们的情况下是电影 ID)时,验证是强制性的。因此,您需要编写一个正则表达式来确保参数中给定的 ID 格式正确。以下是用于验证 ID 的正则表达式示例:

    • 包含字母数字 ID 的模式:[a-zA-Z0-9]+
  • 仅数字 ID 的模式:[0-9]+

  1. 使用Range标头和Query字符串在findAll端点上实现分页系统。

回答:在ScanRequest方法中使用 Limit 选项来限制返回的项目数:

dynamodbClient := dynamodb.New(cfg)
req := dynamodbClient.ScanRequest(&dynamodb.ScanInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
    Limit: aws.Int64(int64(size)),
})

可以从请求标头中读取要返回的项目数:

size, err := strconv.Atoi(request.Headers["Size"])

第七章:实施 CI/CD 流水线

  1. 使用 CodeBuild 和 CodePipeline 为其他 Lambda 函数实现 CI/CD 流水线。

回答FindAllMovies Lambda 函数的 CI/CD 流水线可以按以下方式实现:

version: 0.2
env:
  variables:
    S3_BUCKET: "movies-api-deployment-packages"
    PACKAGE: "github.com/mlabouardy/lambda-codepipeline"

phases:
  install:
    commands:
      - mkdir -p "/go/src/$(dirname ${PACKAGE})"
      - ln -s "${CODEBUILD_SRC_DIR}" "/go/src/${PACKAGE}"
      - go get -u github.com/golang/lint/golint

  pre_build:
    commands:
      - cd "/go/src/${PACKAGE}"
      - go get -t ./...
      - golint -set_exit_status
      - go vet .
      - go test .

  build:
    commands:
      - GOOS=linux go build -o main
      - zip $CODEBUILD_RESOLVED_SOURCE_VERSION.zip main
      - aws s3 cp $CODEBUILD_RESOLVED_SOURCE_VERSION.zip s3://$S3_BUCKET/

  post_build:
    commands:
      - aws lambda update-function-code --function-name FindAllMovies --s3-bucket $S3_BUCKET --s3-key $CODEBUILD_RESOLVED_SOURCE_VERSION.zip

InsertMovie Lambda 函数的 CI/CD 流水线可以按以下方式实现:

version: 0.2
env:
  variables:
    S3_BUCKET: "movies-api-deployment-packages"
    PACKAGE: "github.com/mlabouardy/lambda-codepipeline"

phases:
  install:
    commands:
      - mkdir -p "/go/src/$(dirname ${PACKAGE})"
      - ln -s "${CODEBUILD_SRC_DIR}" "/go/src/${PACKAGE}"
      - go get -u github.com/golang/lint/golint

  pre_build:
    commands:
      - cd "/go/src/${PACKAGE}"
      - go get -t ./...
      - golint -set_exit_status
      - go vet .
      - go test .

  build:
    commands:
      - GOOS=linux go build -o main
      - zip $CODEBUILD_RESOLVED_SOURCE_VERSION.zip main
      - aws s3 cp $CODEBUILD_RESOLVED_SOURCE_VERSION.zip s3://$S3_BUCKET/

  post_build:
    commands:
      - aws lambda update-function-code --function-name InsertMovie --s3-bucket $S3_BUCKET --s3-key $CODEBUILD_RESOLVED_SOURCE_VERSION.zip

Updatemovie Lambda 函数的 CI/CD 流水线可以按以下方式实现:

version: 0.2
env:
  variables:
    S3_BUCKET: "movies-api-deployment-packages"
    PACKAGE: "github.com/mlabouardy/lambda-codepipeline"

phases:
  install:
    commands:
      - mkdir -p "/go/src/$(dirname ${PACKAGE})"
      - ln -s "${CODEBUILD_SRC_DIR}" "/go/src/${PACKAGE}"
      - go get -u github.com/golang/lint/golint

  pre_build:
    commands:
      - cd "/go/src/${PACKAGE}"
      - go get -t ./...
      - golint -set_exit_status
      - go vet .
      - go test .

  build:
    commands:
      - GOOS=linux go build -o main
      - zip $CODEBUILD_RESOLVED_SOURCE_VERSION.zip main
      - aws s3 cp $CODEBUILD_RESOLVED_SOURCE_VERSION.zip s3://$S3_BUCKET/

  post_build:
    commands:
      - aws lambda update-function-code --function-name UpdateMovie --s3-bucket $S3_BUCKET --s3-key $CODEBUILD_RESOLVED_SOURCE_VERSION.zip

DeleteMovie Lambda 函数的 CI/CD 流水线可以按以下方式实现:

version: 0.2
env:
  variables:
    S3_BUCKET: "movies-api-deployment-packages"
    PACKAGE: "github.com/mlabouardy/lambda-codepipeline"

phases:
  install:
    commands:
      - mkdir -p "/go/src/$(dirname ${PACKAGE})"
      - ln -s "${CODEBUILD_SRC_DIR}" "/go/src/${PACKAGE}"
      - go get -u github.com/golang/lint/golint

  pre_build:
    commands:
      - cd "/go/src/${PACKAGE}"
      - go get -t ./...
      - golint -set_exit_status
      - go vet .
      - go test .

  build:
    commands:
      - GOOS=linux go build -o main
      - zip $CODEBUILD_RESOLVED_SOURCE_VERSION.zip main
      - aws s3 cp $CODEBUILD_RESOLVED_SOURCE_VERSION.zip s3://$S3_BUCKET/

  post_build:
    commands:
      - aws lambda update-function-code --function-name DeleteMovie --s3-bucket $S3_BUCKET --s3-key $CODEBUILD_RESOLVED_SOURCE_VERSION.zip
  1. 使用 Jenkins Pipeline 实现类似的工作流程。

回答:我们可以使用 Jenkins 并行阶段功能并行运行代码块,如下所示:

def bucket = 'movies-api-deployment-packages'

node('slave-golang'){
    stage('Checkout'){
        checkout scm
        sh 'go get -u github.com/golang/lint/golint'
        sh 'go get -t ./...'
    }

    stage('Test'){
        parallel {
            stage('FindAllMovies') {
                sh 'cd findAll'
                sh 'golint -set_exit_status'
                sh 'go vet .'
                sh 'go test .'
            }
            stage('DeleteMovie') {
                sh 'cd delete'
                sh 'golint -set_exit_status'
                sh 'go vet .'
                sh 'go test .'
            }
            stage('UpdateMovie') {
                sh 'cd update'
                sh 'golint -set_exit_status'
                sh 'go vet .'
                sh 'go test .'
            }
            stage('InsertMovie') {
                sh 'cd insert'
                sh 'golint -set_exit_status'
                sh 'go vet .'
                sh 'go test .'
            }
        }
    }

    stage('Build'){
        parallel {
            stage('FindAllMovies') {
                sh 'cd findAll'
                sh 'GOOS=linux go build -o main main.go'
                sh "zip findAll-${commitID()}.zip main"
            }
            stage('DeleteMovie') {
                sh 'cd delete'
                sh 'GOOS=linux go build -o main main.go'
                sh "zip delete-${commitID()}.zip main"
            }
            stage('UpdateMovie') {
                sh 'cd update'
                sh 'GOOS=linux go build -o main main.go'
                sh "zip update-${commitID()}.zip main"
            }
            stage('InsertMovie') {
                sh 'cd insert'
                sh 'GOOS=linux go build -o main main.go'
                sh "zip insert-${commitID()}.zip main"
            }
        }
    }

    stage('Push'){
        parallel {
            stage('FindAllMovies') {
                sh 'cd findAll'
                sh "aws s3 cp findAll-${commitID()}.zip s3://${bucket}"
            }
            stage('DeleteMovie') {
                sh 'cd delete'
                sh "aws s3 cp delete-${commitID()}.zip s3://${bucket}"
            }
            stage('UpdateMovie') {
                sh 'cd update'
                sh "aws s3 cp update-${commitID()}.zip s3://${bucket}"
            }
            stage('InsertMovie') {
                sh 'cd insert'
                sh "aws s3 cp insert-${commitID()}.zip s3://${bucket}"
            }
        }
    }

    stage('Deploy'){
        parallel {
            stage('FindAllMovies') {
                sh 'cd findAll'
                sh "aws lambda update-function-code --function-name FindAllMovies \
                --s3-bucket ${bucket} \
                --s3-key findAll-${commitID()}.zip \
                --region us-east-1"
            }
            stage('DeleteMovie') {
                sh 'cd delete'
                sh "aws lambda update-function-code --function-name DeleteMovie \
                --s3-bucket ${bucket} \
                --s3-key delete-${commitID()}.zip \
                --region us-east-1"
            }
            stage('UpdateMovie') {
                sh 'cd update'
                sh "aws lambda update-function-code --function-name UpdateMovie \
                --s3-bucket ${bucket} \
                --s3-key update-${commitID()}.zip \
                --region us-east-1"
            }
            stage('InsertMovie') {
                sh 'cd insert'
                sh "aws lambda update-function-code --function-name InsertMovie \
                --s3-bucket ${bucket} \
                --s3-key insert-${commitID()}.zip \
                --region us-east-1"
            }
        }
    }
}

def commitID() {
    sh 'git rev-parse HEAD > .git/commitID'
    def commitID = readFile('.git/commitID').trim()
    sh 'rm .git/commitID'
    commitID
}
  1. 使用 CircleCI 实现相同的流水线。

回答:CircleCI 工作流选项可用于定义一组构建作业:

version: 2
jobs:
  build_findall:
    docker:
      - image: golang:1.8

    working_directory: /go/src/github.com/mlabouardy/lambda-circleci

    build_dir: findAll

    environment:
        S3_BUCKET: movies-api-deployment-packages

    steps:
      - checkout

      - run:
         name: Install AWS CLI & Zip
         command: |
          apt-get update
          apt-get install -y zip python-pip python-dev
          pip install awscli

      - run:
          name: Test
          command: |
           go get -u github.com/golang/lint/golint
           go get -t ./...
           golint -set_exit_status
           go vet .
           go test .

      - run:
         name: Build
         command: |
          GOOS=linux go build -o main main.go
          zip $CIRCLE_SHA1.zip main

      - run:
          name: Push
          command: aws s3 cp $CIRCLE_SHA1.zip s3://$S3_BUCKET

      - run:
          name: Deploy
          command: |
            aws lambda update-function-code --function-name FindAllMovies \
                --s3-bucket $S3_BUCKET \
                --s3-key $CIRCLE_SHA1.zip --region us-east-1

  build_insert:
    docker:
      - image: golang:1.8

    working_directory: /go/src/github.com/mlabouardy/lambda-circleci

    build_dir: insert

    environment:
        S3_BUCKET: movies-api-deployment-packages

    steps:
      - checkout

      - run:
         name: Install AWS CLI & Zip
         command: |
          apt-get update
          apt-get install -y zip python-pip python-dev
          pip install awscli

      - run:
          name: Test
          command: |
           go get -u github.com/golang/lint/golint
           go get -t ./...
           golint -set_exit_status
           go vet .
           go test .

      - run:
         name: Build
         command: |
          GOOS=linux go build -o main main.go
          zip $CIRCLE_SHA1.zip main

      - run:
          name: Push
          command: aws s3 cp $CIRCLE_SHA1.zip s3://$S3_BUCKET

      - run:
          name: Deploy
          command: |
            aws lambda update-function-code --function-name InsertMovie \
                --s3-bucket $S3_BUCKET \
                --s3-key $CIRCLE_SHA1.zip --region us-east-1

  build_update:
    ...

  build_delete:
    ...

workflows:
  version: 2
  build_api:
    jobs:
      - build_findall
      - build_insert
      - build_update
      - build_delete
  1. 在现有流水线中添加新阶段,如果当前的 git 分支是主分支,则发布新版本。

回答

version: 2
jobs:
  build:
    docker:
      - image: golang:1.8

    working_directory: /go/src/github.com/mlabouardy/lambda-circleci

    environment:
        S3_BUCKET: movies-api-deployment-packages

    steps:
      - checkout

      - run:
         name: Install AWS CLI & Zip
         ...

      - run:
          name: Test
          ...

      - run:
         name: Build
         ...

      - run:
          name: Push
          ...

      - run:
          name: Deploy
          ...

      - run:
          name: Publish
          command: |
            if [ $CIRCLE_BRANCH = 'master' ]; then 
              aws lambda publish-version --function-name FindAllMovies \
                --description $GIT_COMMIT_DESC --region us-east-1
            fi
          environment:
            GIT_COMMIT_DESC: git log --format=%B -n 1 $CIRCLE_SHA1
  1. 配置流水线,每次部署或更新 Lambda 函数时都在 Slack 频道上发送通知。

回答:您可以使用 Slack API 在部署步骤结束时向 Slack 频道发布消息:

- run:
    name: Deploy
    command: |
      aws lambda update-function-code --function-name FindAllMovies \
          --s3-bucket $S3_BUCKET \
          --s3-key $CIRCLE_SHA1.zip --region us-east-1
      curl -X POST -d '{"token":"$TOKEN", "channel":"$CHANNEL", "text":"FindAllMovies has been updated"}' \
           http://slack.com/api/chat.postMessage

第九章:使用 S3 构建前端

  1. 实现一个 Lambda 函数,该函数以电影类别作为输入,并返回与该类别对应的电影列表。

回答

func filter(category string)(events.APIGatewayProxyResponse, error) {
    ...

    filter: = expression.Name("category").Equal(expression.Value(category))
    projection: = expression.NamesList(expression.Name("id"), expression.Name("name"), expression.Name("description"))
    expr, err: = expression.NewBuilder().WithFilter(filter).WithProjection(projection).Build()
    if err != nil {
        return events.APIGatewayProxyResponse {
            StatusCode: http.StatusInternalServerError,
            Body: "Error while building DynamoDB expression",
        }, nil
    }

    svc: = dynamodb.New(cfg)
    req: = svc.ScanRequest( & dynamodb.ScanInput {
        TableName: aws.String(os.Getenv("TABLE_NAME")),
        ExpressionAttributeNames: expr.Names(),
        ExpressionAttributeValues: expr.Values(),
        FilterExpression: expr.Filter(),
        ProjectionExpression: expr.Projection(),
    })

    ...
}
  1. 实现一个 Lambda 函数,该函数以电影的标题作为输入,并返回所有标题中包含关键字的电影。

回答

func filter(keyword string) (events.APIGatewayProxyResponse, error) {
  ...

  filter := expression.Name("name").Contains(keyword)
  projection := expression.NamesList(expression.Name("id"), expression.Name("name"), expression.Name("description"))
  expr, err := expression.NewBuilder().WithFilter(filter).WithProjection(projection).Build()
  if err != nil {
    return events.APIGatewayProxyResponse{
      StatusCode: http.StatusInternalServerError,
      Body: "Error while building DynamoDB expression",
    }, nil
  }

  svc := dynamodb.New(cfg)
  req := svc.ScanRequest(&dynamodb.ScanInput{
    TableName: aws.String(os.Getenv("TABLE_NAME")),
    ExpressionAttributeNames: expr.Names(),
    ExpressionAttributeValues: expr.Values(),
    FilterExpression: expr.Filter(),
    ProjectionExpression: expr.Projection(),
  })
  ... 
}
  1. 在 Web 应用程序上实现删除按钮,通过调用 API Gateway 中的 DeleteMovie Lambda 函数来删除电影。

回答:更新 MoviesAPI 服务以包括以下函数:

delete(id: string){
    return this.http
      .delete(`${environment.api}/${id}`, {headers: this.getHeaders()})
      .map(res => {
        return res
      })
}
  1. 在 Web 应用程序上实现编辑按钮,允许用户更新电影属性。

回答

update(movie: Movie){
    return this.http
      .put(environment.api, JSON.stringify(movie), {headers: this.getHeaders()})
      .map(res => {
        return res
      })
}
  1. 使用 CircleCI、Jenkins 或 CodePipeline 实现 CI/CD 工作流,自动化生成和部署 API Gateway 文档。

回答

def bucket = 'movies-api-documentation'
def api_id = ''

node('slaves'){
  stage('Generate'){
    if (env.BRANCH_NAME == 'master') {
      sh "aws apigateway get-export --rest-api-id ${api_id} \
        --stage-name production \
        --export-type swagger swagger.json"
    }
    else if (env.BRANCH_NAME == 'preprod') {
      sh "aws apigateway get-export --rest-api-id ${api_id} \
        --stage-name staging \
        --export-type swagger swagger.json"
    } else {
      sh "aws apigateway get-export --rest-api-id ${api_id} \
        --stage-name sandbox \
        --export-type swagger swagger.json"
    }
  }

  stage('Publish'){
    sh "aws s3 cp swagger.json s3://${bucket}"
  }
}

第十章:测试您的无服务器应用程序

  1. UpdateMovie Lambda 函数编写一个单元测试。

回答

package main

import (
  "testing"

  "github.com/stretchr/testify/assert"

  "github.com/aws/aws-lambda-go/events"
)

func TestUpdate_InvalidPayLoad(t *testing.T) {
  input := events.APIGatewayProxyRequest{
    Body: "{'name': 'avengers'}",
  }
  expected := events.APIGatewayProxyResponse{
    StatusCode: 400,
    Body: "Invalid payload",
  }
  response, _ := update(input)
  assert.Equal(t, expected, response)
}

func TestUpdate_ValidPayload(t *testing.T) {
  input := events.APIGatewayProxyRequest{
    Body: "{\"id\":\"40\", \"name\":\"Thor\", \"description\":\"Marvel movie\", \"cover\":\"poster url\"}",
  }
  expected := events.APIGatewayProxyResponse{
    Body: "{\"id\":\"40\", \"name\":\"Thor\", \"description\":\"Marvel movie\", \"cover\":\"poster url\"}",
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    },
  }
  response, _ := update(input)
  assert.Equal(t, expected, response)
}
  1. DeleteMovie Lambda 函数编写一个单元测试。

回答

package main

import (
  "testing"

  "github.com/stretchr/testify/assert"

  "github.com/aws/aws-lambda-go/events"
)

func TestDelete_InvalidPayLoad(t *testing.T) {
  input := events.APIGatewayProxyRequest{
    Body: "{'name': 'avengers'}",
  }
  expected := events.APIGatewayProxyResponse{
    StatusCode: 400,
    Body: "Invalid payload",
  }
  response, _ := delete(input)
  assert.Equal(t, expected, response)
}

func TestDelete_ValidPayload(t *testing.T) {
  input := events.APIGatewayProxyRequest{
    Body: "{\"id\":\"40\", \"name\":\"Thor\", \"description\":\"Marvel movie\", \"cover\":\"poster url\"}",
  }
  expected := events.APIGatewayProxyResponse{
    StatusCode: 200,
    Headers: map[string]string{
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*",
    },
  }
  response, _ := delete(input)
  assert.Equal(t, expected, response)
}
  1. 修改之前章节中提供的 Jenkinsfile,包括执行自动化单元测试的步骤。

回答:请注意在 测试 阶段中使用 go test 命令:

def bucket = 'movies-api-deployment-packages'

node('slave-golang'){
  stage('Checkout'){
    checkout scm
  }

  stage('Test'){
    sh 'go get -u github.com/golang/lint/golint'
    sh 'go get -t ./...'
    sh 'golint -set_exit_status'
    sh 'go vet .'
    sh 'go test .'
  }

  stage('Build'){
    sh 'GOOS=linux go build -o main main.go'
    sh "zip ${commitID()}.zip main"
  }

  stage('Push'){
    sh "aws s3 cp ${commitID()}.zip s3://${bucket}"
  }

  stage('Deploy'){
    sh "aws lambda update-function-code --function-name FindAllMovies \
      --s3-bucket ${bucket} \
      --s3-key ${commitID()}.zip \
      --region us-east-1"
  }
}

def commitID() {
  sh 'git rev-parse HEAD > .git/commitID'
  def commitID = readFile('.git/commitID').trim()
  sh 'rm .git/commitID'
  commitID
}
  1. 修改 buildspec.yml 定义文件,包括在将部署包推送到 S3 之前执行单元测试的步骤。

回答

version: 0.2
env:
  variables:
    S3_BUCKET: "movies-api-deployment-packages"
    PACKAGE: "github.com/mlabouardy/lambda-codepipeline"

phases:
  install:
    commands:
      - mkdir -p "/go/src/$(dirname ${PACKAGE})"
      - ln -s "${CODEBUILD_SRC_DIR}" "/go/src/${PACKAGE}"
      - go get -u github.com/golang/lint/golint

  pre_build:
    commands:
      - cd "/go/src/${PACKAGE}"
      - go get -t ./...
      - golint -set_exit_status
      - go vet .
      - go test .

  build:
    commands:
      - GOOS=linux go build -o main
      - zip $CODEBUILD_RESOLVED_SOURCE_VERSION.zip main
      - aws s3 cp $CODEBUILD_RESOLVED_SOURCE_VERSION.zip s3://$S3_BUCKET/

  post_build:
    commands:
      - aws lambda update-function-code --function-name FindAllMovies --s3-bucket $S3_BUCKET --s3-key $CODEBUILD_RESOLVED_SOURCE_VERSION.zip
  1. 为在之前章节中实现的每个 Lambda 函数编写一个 SAM 模板文件。

回答:以下是 FindAllMovies Lambda 函数的 SAM 模板文件;可以使用相同的资源来创建其他函数:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Parameters:
  StageName:
    Type: String
    Default: staging
    Description: The API Gateway deployment stage

Resources:
  FindAllMovies:
    Type: AWS::Serverless::Function
    Properties:
      Handler: main
      Runtime: go1.x
      Role: !GetAtt FindAllMoviesRole.Arn 
      CodeUri: ./findall/deployment.zip
      Environment:
        Variables: 
          TABLE_NAME: !Ref MoviesTable
      Events:
        AnyRequest:
          Type: Api
          Properties:
            Path: /movies
            Method: GET
            RestApiId:
              Ref: MoviesAPI

  FindAllMoviesRole:
   Type: "AWS::IAM::Role"
   Properties:
     Path: "/"
     ManagedPolicyArns:
         - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
     AssumeRolePolicyDocument:
       Version: "2012-10-17"
       Statement:
         -
           Effect: "Allow"
           Action:
             - "sts:AssumeRole"
           Principal:
             Service:
               - "lambda.amazonaws.com"
     Policies: 
        - 
          PolicyName: "PushCloudWatchLogsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: "*"
        - 
          PolicyName: "ScanDynamoDBTablePolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - dynamodb:Scan
                Resource: "*"

  MoviesTable: 
     Type: AWS::Serverless::SimpleTable
     Properties:
       PrimaryKey:
         Name: ID
         Type: String
       ProvisionedThroughput:
         ReadCapacityUnits: 5
         WriteCapacityUnits: 5

  MoviesAPI:
    Type: 'AWS::Serverless::Api'
    Properties:
      StageName: !Ref StageName
      DefinitionBody:
        swagger: 2.0
        info:
          title: !Sub API-${StageName}
        paths:
          /movies:
            x-amazon-apigateway-any-method:
              produces:
                - application/json
              x-amazon-apigateway-integration:
                uri:
                  !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${FindAllMovies.Arn}:current/invocations"
                passthroughBehavior: when_no_match
                httpMethod: POST
                type: aws_proxy

第十二章:保护您的无服务器应用程序

  1. 将用户池中的用户与身份池集成,允许用户使用其 Facebook 帐户登录。

回答:为了将 Facebook 与 Amazon Cognito 身份池集成,您必须遵循给定的步骤:

  • 复制应用程序 ID 和密钥。

  • 在 Amazon Cognito 控制台中配置 Facebook 作为提供者:

  • 用户经过身份验证后,将返回一个 Facebook 会话令牌;必须将此令牌添加到 Amazon Cognito 凭据提供程序中以获取 JWT 令牌。

  • 最后,将 JWT 令牌添加到 API Gateway 请求的 Authorization 标头中。

  1. 将用户池中的用户与身份池集成,允许用户使用其 Twitter 帐户登录。

回答:Amazon Cognito 不支持 Twitter 作为默认的身份验证提供者。因此,您需要使用 OpenID Connect 来扩展 Amazon Cognito:

  1. 将用户池中的用户与身份池集成,允许用户使用其 Google 帐户登录。
  • 在 API 和身份验证下启用 Google API,然后创建 OAuth 2.0 客户端 ID。

  • 在 Amazon Cognito 控制台中配置 Google:

  1. 实现一个表单,允许用户在 Web 应用程序上创建帐户,以便他们能够登录。

答案:可以创建一个基于 Go 的 Lambda 函数来处理帐户创建工作流程。函数的入口点如下所示:

package main

import (
  "os"

  "github.com/aws/aws-lambda-go/lambda"
  "github.com/aws/aws-sdk-go-v2/aws"
  "github.com/aws/aws-sdk-go-v2/aws/external"
  "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
)

type Account struct {
  Username string `json:"username"`
  Password string `json:"password"`
}

func signUp(account Account) error {
  cfg, err := external.LoadDefaultAWSConfig()
  if err != nil {
    return err
  }

  cognito := cognitoidentityprovider.New(cfg)
  req := cognito.SignUpRequest(&cognitoidentityprovider.SignUpInput{
    ClientId: aws.String(os.Getenv("COGNITO_CLIENT_ID")),
    Username: aws.String(account.Username),
    Password: aws.String(account.Password),
  })
  _, err = req.Send()
  if err != nil {
    return err
  }
  return nil
}

func main() {
  lambda.Start(signUp)
}
  1. 为未经身份验证的用户实现忘记密码流程。

答案:可以创建一个基于 Go 的 Lambda 函数来重置用户密码。函数的入口点如下所示:

package main

import (
  "os"

  "github.com/aws/aws-lambda-go/lambda"
  "github.com/aws/aws-sdk-go-v2/aws"
  "github.com/aws/aws-sdk-go-v2/aws/external"
  "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
)

type Account struct {
  Username string `json:"username"`
}

func forgotPassword(account Account) error {
  cfg, err := external.LoadDefaultAWSConfig()
  if err != nil {
    return err
  }

  cognito := cognitoidentityprovider.New(cfg)
  req := cognito.ForgotPasswordRequest(&cognitoidentityprovider.ForgotPasswordInput{
    ClientId: aws.String(os.Getenv("COGNITO_CLIENT_ID")),
    Username: aws.String(account.Username),
  })
  _, err = req.Send()
  if err != nil {
    return err
  }

  return nil
}

func main() {
  lambda.Start(forgotPassword)
}

第十四章:

  1. 编写一个 Terraform 模板来创建InsertMovie Lambda 函数资源。

答案:为 Lambda 函数设置执行角色:

resource "aws_iam_role" "role" {
  name = "InsertMovieRole"
  assume_role_policy = "${file("assume-role-policy.json")}"
}

resource "aws_iam_policy" "cloudwatch_policy" {
  name = "PushCloudWatchLogsPolicy"
  policy = "${file("cloudwatch-policy.json")}"
}

resource "aws_iam_policy" "dynamodb_policy" {
  name = "ScanDynamoDBPolicy"
  policy = "${file("dynamodb-policy.json")}"
}

resource "aws_iam_policy_attachment" "cloudwatch-attachment" {
  name = "cloudwatch-lambda-attchment"
  roles = ["${aws_iam_role.role.name}"]
  policy_arn = "${aws_iam_policy.cloudwatch_policy.arn}"
}

resource "aws_iam_policy_attachment" "dynamodb-attachment" {
  name = "dynamodb-lambda-attchment"
  roles = ["${aws_iam_role.role.name}"]
  policy_arn = "${aws_iam_policy.dynamodb_policy.arn}"
}

接下来,创建 Lambda 函数:

resource "aws_lambda_function" "insert" {
  function_name = "InsertMovie"
  handler = "main"
  filename = "function/deployment.zip"
  runtime = "go1.x"
  role = "${aws_iam_role.role.arn}"

  environment {
    variables {
      TABLE_NAME = "movies"
    }
  }
}

在 REST API 的/movies资源上公开一个POST方法:

resource "aws_api_gateway_method" "proxy" {
  rest_api_id = "${var.rest_api_id}"
  resource_id = "${var.resource_id}"
  http_method = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda" {
  rest_api_id = "${var.rest_api_id}"
  resource_id = "${var.resource_id}"
  http_method = "${aws_api_gateway_method.proxy.http_method}"

  integration_http_method = "POST"
  type = "AWS_PROXY"
  uri = "${aws_lambda_function.insert.invoke_arn}"
}

resource "aws_lambda_permission" "apigw" {
  statement_id = "AllowAPIGatewayInvoke"
  action = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.insert.arn}"
  principal = "apigateway.amazonaws.com"

  source_arn = "${var.execution_arn}/*/*"
}
  1. 更新 CloudFormation 模板,以响应传入的 HTTP 请求,触发已定义的 Lambda 函数与 API Gateway。

答案:将以下属性添加到“资源”部分:

API:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
        Name: API
        FailOnWarnings: 'true'
DemoResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
        ParentId:
            'Fn::GetAtt': [API, RootResourceId]
        PathPart: demo
        RestApiId:
            Ref: API
DisplayMessageMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
        HttpMethod: GET
        AuthorizationType: NONE
        ResourceId:
            Ref: DemoResource
        RestApiId:
            Ref: API
        Integration:
            Type: AWS
            Uri: {'Fn::Join': ["", "- \"arn:aws:apigateway:\"\n- !Ref \"AWS::Region\"\n- \":lambda:path/\"\n- \"/2015-03-31/functions/\"\n- Fn::GetAtt:\n - HelloWorldFunction\n - Arn\n- \"/invocations\""]}
            IntegrationHttpMethod: GET
  1. 编写 SAM 文件,对构建通过本书构建的无服务器 API 所需的所有资源进行建模和定义。

答案

Resources:
  FindAllMovies:
    Type: AWS::Serverless::Function
    Properties:
      Handler: main
      Runtime: go1.x
      Role: !GetAtt FindAllMoviesRole.Arn 
      CodeUri: ./findall/deployment.zip
      Environment:
        Variables: 
          TABLE_NAME: !Ref MoviesTable
      Events:
        AnyRequest:
          Type: Api
          Properties:
            Path: /movies
            Method: GET
            RestApiId:
              Ref: MoviesAPI

  InsertMovie:
    Type: AWS::Serverless::Function
    Properties:
      Handler: main
      Runtime: go1.x
      Role: !GetAtt InsertMovieRole.Arn 
      CodeUri: ./insert/deployment.zip
      Environment:
        Variables: 
          TABLE_NAME: !Ref MoviesTable
      Events:
        AnyRequest:
          Type: Api
          Properties:
            Path: /movies
            Method: POST
            RestApiId:
              Ref: MoviesAPI

  DeleteMovie:
    Type: AWS::Serverless::Function
    Properties:
      Handler: main
      Runtime: go1.x
      Role: !GetAtt DeleteMovieRole.Arn 
      CodeUri: ./delete/deployment.zip
      Environment:
        Variables: 
          TABLE_NAME: !Ref MoviesTable
      Events:
        AnyRequest:
          Type: Api
          Properties:
            Path: /movies
            Method: DELETE
            RestApiId:
              Ref: MoviesAPI

  UpdateMovie:
    Type: AWS::Serverless::Function
    Properties:
      Handler: main
      Runtime: go1.x
      Role: !GetAtt UpdateMovieRole.Arn 
      CodeUri: ./update/deployment.zip
      Environment:
        Variables: 
          TABLE_NAME: !Ref MoviesTable
      Events:
        AnyRequest:
          Type: Api
          Properties:
            Path: /movies
            Method: PUT
            RestApiId:
              Ref: MoviesAPI
  1. 配置 Terraform 以将生成的状态文件存储在远程 S3 后端。

答案:使用以下 AWS CLI 命令创建一个 S3 存储桶:

aws s3 mb s3://terraform-state-files --region us-east-1

在存储桶上启用服务器端加密:

aws s3api put-bucket-encryption --bucket terraform-state-files \
    --server-side-encryption-configuration file://config.json

加密机制设置为 AES-256:

{
  "Rules": [
    {
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      }
    }
  ]
}

配置 Terraform 以使用先前定义的存储桶:

terraform {
  backend "s3" {
    bucket = "terraform-state-files"
    key = "KEY_NAME"
    region = "us-east-1"
  }
}
  1. 为通过本书构建的无服务器 API 创建 CloudFormation 模板。

答案

AWSTemplateFormatVersion: "2010-09-09"
Description: "Simple Lambda Function"
Parameters:
  BucketName:
    Description: "S3 Bucket name"
    Type: "String"
  TableName:
    Description: "DynamoDB Table Name"
    Type: "String"
    Default: "movies"
Resources:
  FindAllMoviesRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - 
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - 
          PolicyName: "PushCloudWatchLogsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: "*"
        - 
          PolicyName: "ScanDynamoDBTablePolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - dynamodb:Scan
                Resource: "*"
  FindAllMovies:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        S3Bucket: !Ref BucketName
        S3Key: findall-deployment.zip
      FunctionName: "FindAllMovies"
      Handler: "main"
      Runtime: "go1.x"
      Role: !GetAtt FindAllMoviesRole.Arn
      Environment:
        Variables:
          TABLE_NAME: !Ref TableName

  InsertMovieRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - 
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - 
          PolicyName: "PushCloudWatchLogsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: "*"
        - 
          PolicyName: "PutItemDynamoDBTablePolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - dynamodb:PutItem
                Resource: "*"
  InsertMovie:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        S3Bucket: !Ref BucketName
        S3Key: insert-deployment.zip
      FunctionName: "InsertMovie"
      Handler: "main"
      Runtime: "go1.x"
      Role: !GetAtt InsertMovieRole.Arn
      Environment:
        Variables:
          TABLE_NAME: !Ref TableName

  UpdateMovieRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - 
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - 
          PolicyName: "PushCloudWatchLogsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: "*"
        - 
          PolicyName: "PutItemDynamoDBTablePolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - dynamodb:PutItem
                Resource: "*"
  UpdateMovie:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        S3Bucket: !Ref BucketName
        S3Key: update-deployment.zip
      FunctionName: "UpdateMovie"
      Handler: "main"
      Runtime: "go1.x"
      Role: !GetAtt UpdateMovieRole.Arn
      Environment:
        Variables:
          TABLE_NAME: !Ref TableName

  DeleteMovieRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - 
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - 
          PolicyName: "PushCloudWatchLogsPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - logs:CreateLogGroup
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: "*"
        - 
          PolicyName: "DeleteItemDynamoDBTablePolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                - dynamodb:DeleteItem
                Resource: "*"
  DeleteMovie:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        S3Bucket: !Ref BucketName
        S3Key: update-deployment.zip
      FunctionName: "DeleteMovie"
      Handler: "main"
      Runtime: "go1.x"
      Role: !GetAtt DeleteMovieRole.Arn
      Environment:
        Variables:
          TABLE_NAME: !Ref TableName

  MoviesApi:
    Type: "AWS::ApiGateway::RestApi"
    Properties:
      Name: "MoviesApi"
      FailOnWarnings: "true"
  MoviesResource:
    Type: "AWS::ApiGateway::Resource"
    Properties:
      ParentId:
        Fn::GetAtt:
          - "MoviesApi"
          - "RootResourceId"
      PathPart: "movies"
      RestApiId:
        Ref: MoviesApi
  CreateMovieMethod:
    Type: "AWS::ApiGateway::Method"
    Properties:
      HttpMethod: "POST"
      AuthorizationType: "NONE"
      ResourceId:
        Ref: MoviesResource
      RestApiId:
        Ref: MoviesApi
      Integration:
        Type: "AWS"
        Uri:
          Fn::Join:
            - ""
            - - "arn:aws:apigateway:"
              - !Ref "AWS::Region"
              - ":lambda:path/"
              - "/2015-03-31/functions/"
              - Fn::GetAtt:
                - InsertMovie
                - Arn
              - "/invocations"
        IntegrationHttpMethod: "POST"
  DeleteMovieMethod:
    Type: "AWS::ApiGateway::Method"
    Properties:
      HttpMethod: "DELETE"
      AuthorizationType: "NONE"
      ResourceId:
        Ref: MoviesResource
      RestApiId:
        Ref: MoviesApi
      Integration:
        Type: "AWS"
        Uri:
          Fn::Join:
            - ""
            - - "arn:aws:apigateway:"
              - !Ref "AWS::Region"
              - ":lambda:path/"
              - "/2015-03-31/functions/"
              - Fn::GetAtt:
                - DeleteMovie
                - Arn
              - "/invocations"
        IntegrationHttpMethod: "DELETE"
  UpdateMovieMethod:
    Type: "AWS::ApiGateway::Method"
    Properties:
      HttpMethod: "PUT"
      AuthorizationType: "NONE"
      ResourceId:
        Ref: MoviesResource
      RestApiId:
        Ref: MoviesApi
      Integration:
        Type: "AWS"
        Uri:
          Fn::Join:
            - ""
            - - "arn:aws:apigateway:"
              - !Ref "AWS::Region"
              - ":lambda:path/"
              - "/2015-03-31/functions/"
              - Fn::GetAtt:
                - UpdateMovie
                - Arn
              - "/invocations"
        IntegrationHttpMethod: "PUT"
  ListMoviesMethod:
    Type: "AWS::ApiGateway::Method"
    Properties:
      HttpMethod: "GET"
      AuthorizationType: "NONE"
      ResourceId:
        Ref: MoviesResource
      RestApiId:
        Ref: MoviesApi
      Integration:
        Type: "AWS"
        Uri:
          Fn::Join:
            - ""
            - - "arn:aws:apigateway:"
              - !Ref "AWS::Region"
              - ":lambda:path/"
              - "/2015-03-31/functions/"
              - Fn::GetAtt:
                - FindAllMovies
                - Arn
              - "/invocations"
        IntegrationHttpMethod: "GET"

  DynamoDBTable:
    Type: "AWS::DynamoDB::Table"
    Properties:
      TableName: !Ref TableName
      AttributeDefinitions:
        -
          AttributeName: "ID"
          AttributeType: "S"
      KeySchema:
        -
          AttributeName: "ID"
          KeyType: "HASH"
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
  1. 为通过本书构建的无服务器 API 创建 Terraform 模板。

答案:为了避免代码重复,并保持模板文件的清晰和易于遵循和维护,可以使用“循环”,“条件”,“映射”和“列表”来创建已定义的 Lambda 函数的 IAM 角色:

resource "aws_iam_role" "roles" {
  count = "${length(var.functions)}"
  name = "${element(var.functions, count.index)}Role"
  assume_role_policy = "${file("policies/assume-role-policy.json")}"
}

resource "aws_iam_policy" "policies" {
  count = "${length(var.functions)}"
  name = "${element(var.functions, count.index)}Policy"
  policy = "${file("policies/${element(var.functions, count.index)}-policy.json")}"
}

resource "aws_iam_policy_attachment" "policy-attachments" {
  count = "${length(var.functions)}"
  name = "${element(var.functions, count.index)}Attachment"
  roles = ["${element(aws_iam_role.roles.*.name, count.index)}"]
  policy_arn = "${element(aws_iam_policy.policies.*.arn, count.index)}"
}

可以应用相同的方法来创建所需的 Lambda 函数:

resource "aws_lambda_function" "functions" {
  count = "${length(var.functions)}"
  function_name = "${element(var.functions, count.index)}"
  handler = "main"
  filename = "functions/${element(var.functions, count.index)}.zip"
  runtime = "go1.x"
  role = "${element(aws_iam_role.roles.*.arn, count.index)}"

  environment {
    variables {
      TABLE_NAME = "${var.table_name}"
    }
  }
}

最后,可以按以下方式创建 RESTful API:

resource "aws_api_gateway_rest_api" "api" {
  name = "MoviesAPI"
}

resource "aws_api_gateway_resource" "proxy" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  parent_id = "${aws_api_gateway_rest_api.api.root_resource_id}"
  path_part = "movies"
}

resource "aws_api_gateway_deployment" "staging" {
  depends_on = ["aws_api_gateway_integration.integrations"]

  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  stage_name = "staging"
}

resource "aws_api_gateway_method" "proxies" {
  count = "${length(var.functions)}"
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  resource_id = "${aws_api_gateway_resource.proxy.id}"
  http_method = "${lookup(var.methods, element(var.functions, count.index))}"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "integrations" {
  count = "${length(var.functions)}"
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  resource_id = "${element(aws_api_gateway_method.proxies.*.resource_id, count.index)}"
  http_method = "${element(aws_api_gateway_method.proxies.*.http_method, count.index)}"

  integration_http_method = "POST"
  type = "AWS_PROXY"
  uri = "${element(aws_lambda_function.functions.*.invoke_arn, count.index)}"
}

resource "aws_lambda_permission" "permissions" {
  count = "${length(var.functions)}"
  statement_id = "AllowAPIGatewayInvoke"
  action = "lambda:InvokeFunction"
  function_name = "${element(aws_lambda_function.functions.*.arn, count.index)}"
  principal = "apigateway.amazonaws.com"

  source_arn = "${aws_api_gateway_deployment.staging.execution_arn}/*/*"
}
posted @ 2024-05-04 22:35  绝不原创的飞龙  阅读(29)  评论(0编辑  收藏  举报