无服务器应用的-DevOps-全-

无服务器应用的 DevOps(全)

原文:annas-archive.org/md5/3c0d8d1b654bd7429a2508ed40e3fbf9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

无服务器开发为开发者提供了专注于开发本身的自由,无需担心服务器端的事务。本书旨在通过向您展示如何有效地将 DevOps 原则应用于无服务器应用程序,简化您的无服务器部署体验。在每一步过程中,您将接触到有效构建完整持续集成和持续交付管道及日志管理的最佳实践。

本书适用对象

无服务器应用程序的 DevOps 适用于 DevOps 工程师、架构师或任何想了解无服务器世界中 DevOps 思想的人。您将学习如何在无服务器开发中使用 DevOps,并应用持续集成、持续交付、测试、日志管理和监控。

本书内容

第一章,介绍无服务器,用简单的语言描述了什么是无服务器,涵盖了其优缺点。它讨论了不同的无服务器服务提供商,并介绍了他们提供的无服务器服务。

第二章,理解无服务器框架,讨论了不同的无服务器部署框架,并深入探讨了在本书大多数章节中使用的 Serverless Framework。

第三章,将 DevOps 应用于 AWS Lambda 应用程序,深入探讨了 AWS Lambda 与 DevOps 相关的内容。它通过多个实践教程,讲解了如何使用 Serverless Framework 和 Jenkins 构建持续集成与持续部署管道,以及如何进行监控和日志管理。还涵盖了如何为 AWS Lambda 设置金丝雀发布和蓝绿发布。

第四章,与 Azure Functions 一起使用 DevOps,讲解了 Azure Functions。首先介绍如何创建和部署 Azure Functions,然后讲解如何通过 Jenkins 设置持续集成与持续部署管道。还涵盖了监控和日志管理,并讨论了与 Azure Functions 一起使用 DevOps 的最佳实践。

第五章,将 DevOps 与 IBM OpenWhisk 集成,介绍了 OpenWhisk,并讲解了如何使用 Serverless Framework 和 Jenkins 设置部署管道。还涵盖了如何在 IBM Cloud 上监控和动态展示 OpenWhisk。

第六章,与 Google Functions 一起使用 DevOps,首先介绍了 Google Functions。然后通过多个教程,从创建函数到设置自动化部署管道,逐步讲解。它还讲述了如何使用 Google Stackdriver 进行监控和日志管理。

第七章,为 Kubeless 添加 DevOps 元素,解释了 Kubeless 是一个基于 Kubernetes 的开源无服务器架构。本章介绍了 Kubeless,并解释了如何为 Kubeless 设置持续集成和持续部署。

第八章,无服务器与 DevOps 的最佳实践与未来,讨论了我们在开发过程中如何处理应用程序的性能问题,这引出了最佳实践。因此,本章概述了无服务器和 DevOps 的最佳实践,以便我们能够轻松地进行开发和部署。

第九章,用例与基础知识,介绍了一些常见的无服务器用例,并且讲解了能够提高开发和部署效率的一些必要知识点。

第十章,无服务器的 DevOps 趋势,介绍了无服务器如何塑造 DevOps,以及 DevOps 在采用无服务器后需要如何调整其轨迹。

要充分利用本书

本书的主要内容是阐明无服务器及其不同的服务提供商。它还讲解了如何采用自动化方式为无服务器函数设置 DevOps。

你需要了解 DevOps、持续集成和持续部署,并且应该具备一些流行的 DevOps 工具的知识,如 Jenkins、ELK(Elasticsearch、Logstash 和 Kibana)。如果你了解不同的云服务提供商,如 AWS、Azure 和 Google,那将是一个加分项。

我的教程大多数基于 MacBook 和 Docker 容器构建,因此我建议使用某种形式的 Linux 来进行这些教程。

下载示例代码文件

你可以从你的帐户在 www.packt.com 下载本书的示例代码文件。如果你在其他地方购买了本书,你可以访问 www.packt.com/support,并注册以直接通过电子邮件获取文件。

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

  1. 请在 www.packt.com 上登录或注册。

  2. 选择“支持”标签。

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

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

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

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址是 github.com/PacktPublishing/DevOps-for-Serverless-Applications。如果代码有更新,将会在现有的 GitHub 仓库中更新。

我们还从我们丰富的书籍和视频目录中提供其他代码包,可在github.com/PacktPublishing/查看!快去看看吧!

下载彩色图片

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

使用的约定

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

CodeInText:表示文本中的代码字词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这是一个示例:"在语句中,我们将参数传递给 CLI,并且stage值填充在serverless.yml文件中。"

一个代码块设置如下:

# serverless.yml
service: myService
provider:
  name: aws
  runtime: nodejs6.10
  memorySize: 512 # will be inherited by all functions

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

$ pip install zappa

粗体:表示一个新术语,重要词或屏幕上看到的字词。例如,菜单或对话框中的字词以这种方式出现在文本中。这是一个例子:"在左侧边栏点击Users,然后点击Add User按钮,并添加用户名adm-serverless。"

警告或重要提示会出现在这样的地方。

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

联系我们

我们的读者的反馈总是受欢迎的。

一般反馈:电邮至customercare@packtpub.com并在您的消息主题中提到书名。如果您对本书的任何方面有疑问,请发送电子邮件至customercare@packtpub.com与我们联系。

勘误:尽管我们尽最大努力确保内容的准确性,但错误确实偶尔发生。如果您在本书中发现错误,请向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,请提供地址或网站名称。请通过copyright@packt.com与我们联系,并附上链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了这本书,为什么不在购买它的网站上留下评论呢?潜在的读者可以看到并使用您的公正意见来做出购买决策,我们在 Packt 能理解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问packt.com

第一章:引入无服务器架构

本书将带我们进入无服务器信息技术的世界,了解多个不同的云服务提供商,如 AWS、Azure、Google、OpenWhisk 和其他一些服务。我们将详细介绍每个云服务,并探讨将 DevOps 应用于这些服务的不同方法。我们将研究不同的使用案例,并学习每个案例的最佳实践。

本介绍章节将涵盖以下主题:

  • 无服务器简介

  • 核心概念

    • 后端即服务 (BaaS)

    • 功能即服务 (FaaS)

    • AWS Lambda

    • Azure Functions

    • Google Functions

    • OpenWhisk

  • 无服务器的优缺点

  • DevOps 与无服务器架构

无服务器简介

当我们听到“无服务器”这个词时,第一个想到的可能是,哦,我的代码将在没有任何服务器的情况下自动运行! 从某种程度上来说,这个想法是正确的:无服务器架构是一种将代码部署到云端并自动执行的过程,用户无需担心底层基础设施、租赁或购买服务器、扩展、监控或容量规划。服务提供商会负责所有这些事情。而且,你不会相信它是多么便宜,管理起来又是多么轻松。现在,你可能在想,这怎么可能呢? 为了更详细地了解它的工作原理,让我们将无服务器架构与我们日常生活中的一件事进行对比。

无服务器架构有点像处理我们的洗衣服。我们都需要洗衣服,为此,我们需要购买一台洗衣机。但是,这台洗衣机每周的使用时间大约只有 10 到 15 个小时,其余时间它将处于空闲状态。有趣的是,我们购买服务器来托管我们的应用程序,而大部分时间,服务器在等待请求时处于空闲状态,未被使用。我们拥有大量的服务器,但它们很少被管理或淘汰。由于这些服务器没有得到正确的使用或管理,导致电力供应、容量、存储和内存等资源被浪费。

此外,在洗衣服时,洗衣机只能承受一定的负载和容量。同样,服务器也有类似的限制:它们也只能承载一定的负载和流量。负载或流量越大,处理速度可能会变慢,甚至可能完全停止。现在,为了应对额外的负载,我们可能会决定购买一台更大的洗衣机,这样可以处理更多的衣物,支持更大的负载。但同样,这台高端机器在我们洗大量衣物或只洗一件衣物时,都将消耗相同的资源,这是浪费。同样,在服务器的类比中,当我们需要应对更高流量或请求时,我们可能会购买一台高端服务器。但是,即使是高端服务器,我们每天处理 10 个请求与处理 10,000 个请求时,所消耗的资源是一样的。

此外,要使用洗衣机,我们需要在洗衣前分开衣物,选择程序,添加洗衣液和柔顺剂,如果这些操作不当,可能会弄坏衣物。同样,在使用服务器时,我们也必须确保安装正确的软件——以及正确的软件版本——确保其足够安全,并且始终监控服务是否正常运行。

此外,如果你租的是公寓,可能没有洗衣机,或者你可能会发现当你批量洗衣时,自助洗衣店会更便宜,也更省心。所以你可以根据需要租用自助洗衣店或投币式洗衣机。同样,许多公司,如 AWS、Azure 或 Google,最初也是通过租用服务器起步的。所以我们也可以租用服务器,提供商会负责存储、内存、电力和基本设置。

假设我们决定使用本地自助洗衣店的投币式洗衣机作为最佳选择。现在我们只需要投币洗衣,但我们仍然需要确保添加洗衣液和柔顺剂,并设置正确的程序,否则我们会弄坏衣物。同样,当我们在云上租用服务器时,可能不需要处理电力、存储和内存问题,但我们仍然需要安装所需的软件,监控应用服务,定期升级软件版本,并监控应用性能。

假设我找到了一家新的自助洗衣店,这家店提供送货服务,并按每件衣物收费,所以我可以一次送一批衣物或单件衣物。它们还会为我洗衣并熨烫衣物。现在,我不需要担心使用哪种洗衣液或柔顺剂,也不需要担心选择什么清洗程序,甚至不需要拥有熨斗。但在信息技术的世界中,许多公司仍然在使用租赁的投币式洗衣机系统。它们仍然租用服务器并通过平台即服务PaaS)进行管理,仍然需要管理应用程序的停机时间,升级软件版本,并监控服务。

但通过采用无服务器的方法,这一切都可以改变。无服务器计算将自动配置服务器并执行代码。随着流量的增加,它将自动扩展,提供所需的资源,一旦流量减少,它也会缩减规模。

核心概念

在早期,“无服务器”一词指的是依赖第三方应用或服务来管理服务器端逻辑的应用。这些应用通常是基于云的数据库,如 Google Firebase,或者是身份验证服务,如 Auth0 或 AWS Cognito。它们被称为Backend as a ServiceBaaS)服务。但“无服务器”也意味着代码是为事件触发而开发的,并且在无状态的计算容器中执行。这种架构通常被称为Function as a ServiceFaaS)。让我们更详细地了解一下每种类型的服务。

Backend as a Service

BaaS 的概念是由 Auth0 和 Google Firebase 提出的。Auth0 最初作为身份验证即服务(authentication as a service)起步,但后来转向了 FaaS。因此,BaaS 本质上是通过第三方服务来实现我们所需的功能,它为应用的实现提供了服务器端逻辑。

常见的做法是,大多数 Web 和移动应用开发者会自己编写身份验证功能,如登录、注册和密码管理,并且每个服务都有自己的 API,需要将其集成到应用中。但这对开发者来说既复杂又耗时,而 BaaS 提供商通过提供统一的 API 和 SDK,帮助开发者将它们与应用前端连接,从而省去了开发自己的后端服务的麻烦。这样就节省了时间和金钱。

例如,假设我们想要构建一个门户网站,该网站需要进行身份验证才能使用我们的服务。我们需要设置登录、注册和身份验证系统,并且我们还需要让消费者能够通过简单的一键操作,使用他们现有的 Google、Facebook 或 Twitter 账户进行登录。单独开发这些功能需要大量的时间和精力。

但是通过使用 BaaS,我们可以轻松地将我们的门户网站与 Google、Facebook 或 Twitter 账户进行集成,实现注册和身份验证。另一个 BaaS 服务是由 Google 提供的 Firebase。Firebase 是一个数据库服务,广泛应用于移动应用,它减少了数据库管理的开销,并且为不同类型的用户提供授权。简而言之,这就是 BaaS 的工作原理。接下来,让我们来看一下无服务器架构中的 FaaS 部分。

Function as a Service

如本章开头所提到的,FaaS 本质上是一个执行小任务的程序或功能,这些任务由事件触发,与一个执行多种功能的单体应用不同。因此,在 FaaS 架构中,我们将应用拆分成小的、独立的程序或功能,而不是运行在 PaaS 上的、执行多个功能的单体应用。例如,API 中的每个端点都可以是一个单独的函数,我们可以根据需求运行这些函数,而不是一直运行整个应用。

常见的做法是将 API 编码为多层架构,类似三层架构,将代码分解为表示层、业务层和数据层。所有路由都会触发业务层中的相同处理函数,数据将被处理并发送到数据层,该数据层通常是数据库或文件。下图显示了这种三层架构:

对于少量同时用户,这种方式可能工作正常,但当流量呈指数级增长时,我们该如何管理呢?此时,应用将突然成为计算噩梦。因此,理想情况下,我们可以将包含数据库的数据层分离到独立的服务器中来解决这个问题。但是问题依然没有解决,因为 API 路由和业务逻辑仍然在同一个应用中,因此扩展仍然是一个问题。

对于相同的问题,采用无服务器的方法则更加轻松。与其为应用 API 端点和业务逻辑设置一个服务器,不如将应用程序的每个部分拆解成独立的、自动扩展的函数。开发人员编写一个函数,随后无服务器提供商将该函数封装到一个容器中,容器可以在任意数量的服务器上进行监控、克隆和分发,如下图所示:

将应用程序拆分成函数的好处在于,我们可以分别扩展和部署每个函数。例如,如果我们的 API 中某个端点接收到 90%的流量,或者我们的图像处理代码占用了大部分计算时间,那么这个函数或代码片段比扩展整个应用程序更容易进行分发和扩展。

在 FaaS 系统中,函数预计能够在毫秒级别内启动,以便处理单个请求。相比之下,在 PaaS 系统中,通常有一个应用线程,它会长时间运行,并处理多个请求。FaaS 服务按函数的执行时间收费,而 PaaS 服务则按运行服务器应用程序的线程时间收费。

在微服务架构中,应用程序是松散耦合的、精细化的、轻量级的。微服务诞生的原因是将单体应用拆分为小型服务,以便可以独立开发、管理和扩展。但是 FaaS 进一步将这一概念推向了更小的单元,称为函数。

趋势非常明显:工作单元变得越来越小。我们正从单体应用转向微服务,再到现在的函数,如下图所示:

随着容器的兴起,许多云服务商意识到无服务器函数架构将为开发者提供更好的灵活性,让他们构建应用程序时无需担心运维(操作)。AWS 是第一个推出这一服务的公司,命名为 Lambda,之后其他云服务提供商也跟随这一趋势,比如微软 Azure 推出了 Azure Functions,谷歌云推出了 Google Functions。但这一流行趋势也为一些供应商提供了机会,他们开始构建开源版本。一些流行的版本包括 IBM 的 OpenWhisk(基于 Apache 许可),Kubeless(基于 Kubernetes 构建)以及 OpenFaaS(基于 Docker 容器构建)。甚至 Oracle 也加入了这一行列,推出了 Oracle Fn。本章我们将简要介绍每个供应商,了解他们的工作原理。接着,在接下来的书籍中,我们将与这些供应商一起探索他们的 DevOps 方法。

AWS Lambda

亚马逊网络服务 (AWS) 是首个在 2014 年推出 FaaS 或无服务器服务的公司,命名为 Lambda。如今,他们是这一类无服务器服务的领导者。AWS Lambda 采用事件驱动的方法。当事件触发时,Lambda 执行代码并完成所需功能,并且能够在流量增加时自动扩展,流量减少时自动缩减。Lambda 函数根据事件触发运行,例如对 Amazon S3 存储桶中数据的更改、对 Amazon DynamoDB 表的更改,或响应通过 AWS API Gateway 发出的 HTTP 请求。正是通过这种方式,Lambda 帮助构建多个服务的触发器,如 S3、DynamoDB 和 Kinesis 中的流数据存储。

所以,Lambda 帮助开发者只需关注编码——计算部分,如内存、CPU、网络和空间,由 Lambda 自动处理。它还会自动管理函数的修补、日志记录和监控。从架构上讲,Lambda 函数在一个容器中被调用,该容器是基于提供的配置启动的。这些容器可能会被复用来处理后续的函数调用。当需求减少时,容器会被停用,但这一切都由 Lambda 内部管理,因此用户无需担心,因为他们无法控制这些容器。AWS Lambda 函数支持的语言有 Node.js、Java、C# 和 Python。

在构建无服务器应用时,核心组件是函数和事件源。事件源是 AWS 服务或自定义应用程序,而 Lambda 函数处理这些事件。每个 Lambda 函数的执行时间为 300 秒。

让我们来看一个 AWS Lambda 实际运作的示例。在一个照片分享应用中,用户上传照片,这些照片需要生成缩略图,以便在用户的个人主页上展示。在这种情况下,我们可以使用 Lambda 函数来创建缩略图,这样一旦照片被上传到 AWS S3 存储桶中,支持事件源的 S3 就可以发布对象创建事件并调用 Lambda 函数。Lambda 函数的代码从 S3 存储桶中读取最新的照片对象,创建一个缩略图版本,并将其保存到另一个 S3 存储桶中。

在 第三章,将 DevOps 应用到 AWS Lambda 应用程序,我们将探讨如何以自动化的方式创建、运行和部署 Lambda 函数,同时通过日志监控和进行根本原因分析。

Azure Functions

Azure Functions 是微软进入无服务器架构的一个尝试。它于 2016 年 3 月上线。Azure Functions 允许使用 C#、F#、PHP、Node.js、Python 和 Java 编写函数。Azure Functions 还支持 bash、batch 和 PowerShell 文件。Azure Functions 与 Visual Studio Team System (VSTS)、Bitbucket 和 GitHub 无缝集成,这将使得持续集成和持续部署变得更加容易。Azure Functions 支持多种类型的事件触发器、基于时间的任务事件、OneDrive 和 SharePoint,可以配置为触发函数中的操作。实时处理数据和文件增加了操作无服务器机器人的能力,该机器人使用 Cortana 提供信息。微软还推出了 Logic Apps,这是一种带有工作流编排引擎的工具,可以让技术不太熟练的用户构建无服务器应用。Azure Functions 允许在其他 Azure 云服务和 HTTP 请求中创建触发器。每个函数的最大执行时间为五分钟。Azure Functions 提供两种类型的应用服务计划:动态经典应用服务 是一个容器或环境,用于运行一组 Azure 函数。动态选项类似于 Lambda,我们为函数运行时使用的时间和内存付费。经典选项是为函数分配您现有或预配置的应用资源,而无需额外费用。分配的内存按照应用服务进行,而在 AWS Lambda 中,内存分配是按每个函数来计算的。Azure Functions 允许每个函数最多 10 个并发执行。

Azure Functions 的定价模型与 AWS Lambda 类似。总费用是基于触发器执行的次数和时间来计算的。因此,前一百万次请求是免费的,超过这一数量后,每执行 100,000 次将收费 0.02 美元。

我们将在第四章中讨论自动化部署 Azure Functions,DevOps 与 Azure Functions,并探讨不同的 DevOps 过程如何适应,以加速 Azure Functions 的市场发布速度。

Google Functions

与 AWS Lambda 和 Azure Functions 相比,Google 进入这一领域稍晚一些。它们在 2017 年 3 月发布了 Cloud Functions 的 beta 版本。目前,我们可以通过 Node.js 编写 Google Functions;它们很快将支持其他语言。Google Functions 支持内部事件总线触发器和 HTTP 触发器,响应诸如 GitHub WebHooks、Slack 或任何 HTTPS 请求等事件,还支持 Firebase 分析、实时数据库等来自移动端的事件。

在可扩展性方面,Google Functions 内置了自动扩展功能。Google Functions 支持每个项目最多 1,000 个函数,并允许每个函数执行 400 次,据称这是一个软性限制。Google Functions 允许执行时间为 540 秒(9 分钟)。支持通过 ZIP 上传、云存储和云存储库进行部署。事件源通过云 Pub/Sub 或云存储对象进行。函数执行的日志管理通过 Stackdriver logging 来实现,这是 Google Cloud 的日志工具。

我们将在第五章中深入探讨 Google Functions 的 DevOps 方法,将 DevOps 与 IBM – OpenWhisk 集成,并讨论使用 Google Functions 的 DevOps 最佳实践。

OpenWhisk

OpenWhisk 是一个开源的 FaaS 平台,可以部署到云端或本地数据中心。它由 IBM 推动,并且通过 Apache 许可证进行了开源。我们可以通过 Bluemix(Bluemix 是 IBM 的云平台)注册 OpenWhisk,或者通过 vagrant 在本地设置。它的工作方式与任何 FaaS 技术类似,比如 AWS Lambda、Azure Functions 或 Google Functions。但 OpenWhisk 支持开放事件提供者。如果我们有自定义事件提供者,可以将其与 OpenWhisk 集成,因为与其他云平台不同,OpenWhisk 允许其服务内的事件。例如,可以通过 OpenWhisk 在 RSS feed 中出现新条目时触发函数。OpenWhisk 还允许组织在其内部设置自己的 FaaS 平台,如果他们不愿意让数据离开组织。OpenWhisk 支持的语言有 Swift、JavaScript 和 Node.js。OpenWhisk 集成了 Docker 支持,用于函数内部二进制代码的执行。

其他无服务器架构

还有许多其他无服务器选项,如 OpenFaaS、Fission 和 Iron.io。我在本书中不会详细讨论它们,但我们可以浏览它们的功能。OpenFaaS是无服务器架构的一个开源替代方案。它建立在 Docker 容器、Swarm 和 Kubernetes 之上。它有自己的 UI 门户,还支持 CLI 来部署函数。OpenFaaS 支持 Node.js、Python、Go 和 C#,并且可以在 Windows 和 Linux 上运行。我们可以将它部署在云端、本地笔记本电脑或本地服务器上。我们几乎可以为所有事情编写函数——这是 OpenFaaS 的承诺。OpenFaaS 是用 Golang 编写的。它允许通过 HTTP/HTTPS 请求来触发事件。

Fission是另一种开源的无服务器架构——其底层技术是 Kubernetes 和 Docker 容器,可以在云端和本地基础设施上部署。它被设计为一组微服务,其组件包括控制器、路由器和池管理器。路由器管理 HTTP 请求,控制器管理函数、事件触发器和环境镜像,池管理器管理容器池并将函数加载到这些容器中。函数是用 Python 编写的。

无服务器架构的好处

使用无服务器架构有很多优缺点。我们先来看看其中的亮点。为什么有人会选择使用 AWS Lambda 或 OpenWhiz 等无服务器架构来构建他们的应用?主要原因是应用的高效性、快速扩展的能力,以及最重要的,它的成本。让我们先来看几个重要的优点,然后再讨论缺点。

更快的市场推出时间

我们可以更快地将应用推向市场,因为 OPS 变得更加简化,这将帮助开发者专注于他们的开发工作。OPS 团队无需再为编写可以处理扩展的代码或担心底层基础设施而烦恼。

此外,团队可以借助第三方集成(如 OAuth、Twitter 和地图等 API 服务)更快地构建应用。

高度可扩展

每个公司都希望他们的应用表现更好、实现零停机时间,并能够随着流量的增加快速而轻松地扩展,但对于单体应用开发来说,这可能变得非常困难。随着应用负载的增加,Ops 团队必须时刻关注底层基础设施的扩展。由于流量激增而导致的停机时间浪费了大量的时间和资金。而无服务器计算具有高度可扩展性,应用可以在几秒钟内进行扩展和缩减。

低成本

在无服务器计算中,开发人员仅为函数运行时计费,不像 IaaS 和 PaaS 那样对每个服务器进行 24/7 计费。这对那些拥有大量应用、API 或微服务的公司来说非常有利,这些公司目前 24 小时全天候运行,并且始终使用资源,无论是否需要。但通过无服务器,我们可以根据需要执行函数并共享资源,从而减少空闲时间,并且仍然使应用运行得更快,而不是让应用全天候运行。

延迟和地理位置改善

应用的可扩展性取决于三个因素:用户数量、用户位置和网络延迟。在今天的世界里,应用拥有全球用户,这可能会增加延迟。但通过无服务器平台,这种延迟的风险可以得到有效缓解。使用无服务器时,每次事件调用时会实例化一个容器来运行一个函数,这个容器可以创建在离用户地理位置较近的地方,从而自动提高应用的性能。

无服务器的缺点

尽管使用无服务器有好处,但也有缺点。让我们来看一下无服务器功能的另一面。

复杂性增加

我们在应用中越细化,复杂性就越高。每个函数的代码可能会更简洁,但整个应用会变得更复杂。例如,假设我们将应用拆分成 10 个不同的微服务。我们需要管理 10 个不同的应用,而在单体应用中,只需管理一个应用。

工具缺乏

假设我们将单体应用拆分成 50 个不同的函数。我们仍然需要管理、记录、监控和部署这些单体应用,并且有多种过程和工具需要处理。由于无服务器技术在市场上还比较新,当前监控或记录一个只运行几秒钟的应用是有限且具有挑战性的,但随着时间的推移,会有许多高效的方式来解决这个问题。

架构的复杂性

很难决定函数应该有多细化,而且评估、实施和测试以检查我们的偏好是非常耗时的。管理过多的函数会变得繁琐,而忽视细化则会导致我们建立迷你单体应用。

实现中的缺点

使用无服务器的最大挑战是集成测试。我们将为一个应用编写许多函数,但如何将它们集成在一起作为一个应用运行呢?当然,在此之前,如何测试它们协同工作的效率呢?由于无服务器是新的且仍在成熟过程中,测试过程中添加的选项仍然有限。但在未来的章节中,我们将涵盖部署和测试的几个方面。

无服务器与 DevOps

DevOps是另一个流行了很长时间的术语。像无服务器一样,DevOps 也是一个令人困惑的术语。许多人对 DevOps 有不同的看法。有些人认为 DevOps 只是工具,有些人认为 DevOps 由几个过程组成——甚至 IaaS 和 PaaS 也属于 DevOps 的范畴。根据我的理解,DevOps 是工具、过程和反馈的协作,它们共同推动 DevOps 的成功实施。但是,为什么我们要在这里谈论 DevOps 呢?简而言之,因为我们需要 DevOps 来顺利过渡到生产环境,记录或监控无服务器函数,并在它们到达用户之前进行测试。

从 DevOps 的功能角度来看,我将涵盖版本控制、持续集成、持续部署、监控和日志记录,适用于 AWS Lambda 函数、Azure Functions、Google Functions 和 OpenWhiz。版本控制是一个管理代码版本的过程,使我们能够进行分支、打包、部署,同时也能回滚到先前的版本。持续集成是开发人员通过自动化构建将代码集成在一起,以便及早发现和解决问题的做法。持续部署本质上是一个管道,代码通过自动化测试不断精炼,然后被部署到环境中。这个管道平稳地向生产环境推进,手动干预最小化。

总结

我们将在下一章评估几种无服务器框架,并在本书中通过各种 DevOps 实施教程使用其中一个。在这些 DevOps 实施中,我们将使用一些更流行的 DevOps 工具,比如用于协调的 Jenkins 和用于版本管理的 GitHub。我们还将涉及自动化单元测试、集成测试和系统测试。我们还将关注监控和日志记录的最佳实践,以及更多 DevOps 过程和特性。

第二章:了解无服务器框架

在上一章中,我们探讨了无服务器计算的世界,它是如何工作的,目的是什么,采用它的好处,各种服务提供商以及它们在提供服务方面的表现。我们还了解了采用无服务器架构的优缺点。本章的目标是教会我们不同的无服务器部署框架,以及它们最终如何帮助我们实现持续集成和持续交付。此外,我们还将了解框架提供的各种功能,并更详细地讨论无服务器框架,了解它在幕后做了什么。

在应用开发的世界里,开发应用的过程通常是相同的。开发者在本地机器上开发代码,然后将更改编译并推送到源代码管理仓库。测试人员随后测试并发布报告,而运维团队则负责将代码部署到不同的环境中并管理基础设施。

但有可能相同的代码在生产环境中会失败。为了让这段代码重新工作,开发者、测试人员和运维团队必须加班才能使生产环境恢复正常。在根本原因分析中,开发者会说他的代码在个人电脑上运行正常,测试人员会声称她已经测试了所有内容并提供支持这一事实的报告,而运维人员则会说他的工作只是部署代码。因此,我们面临的挑战如下:

  • 确保每次部署时代码都能在生产环境中完美运行

  • 加速部署周期

  • 让团队一起合作,承担各自的责任

  • 实现无摩擦的生产部署

解决这些问题的方法是采用 DevOps,自动化流程和团队协作。

"DevOps 是一套自动化软件开发和 IT 团队之间流程的实践,以便他们能够更快、更可靠地构建、测试和发布软件。"

  • 在 Atlassian 上的 DevOps 定义

DevOps 依赖于工具、人员、流程和反馈循环的结合。但工具和流程是 DevOps 的前轮,在非生产和生产环境中推动更快的发布周期、持续集成、持续测试和持续部署方面发挥着至关重要的作用。

使用无服务器架构来实现 DevOps 要容易得多,因为我们不必担心底层基础设施。然而,我们仍然需要持续集成、监控、日志记录以及持续部署,以便代码顺利推向生产环境。由于无服务器架构仍处于初期阶段,现有的一些新开发工具和框架数量有限,但这些数量最终会增长。我们将关注更流行的工具或框架,最后集中讨论一个框架,深入了解其功能。

本书中我们将要探讨的所有工具都是开源框架。它们各自根据需求提供特定功能。我们将考虑四个流行的无服务器框架,并了解它们的特点。

ClaudiaJS

ClaudiaJS是最早的部署框架和工具之一。它是开源许可证下的,且在撰写本文时,仅支持 AWS Lambda。ClaudiaJS 是一个 Node.js 库,帮助将 Node.js 项目部署到 AWS Lambda 和 API Gateway。它目前仅支持 Node.js 语言。ClaudiaJS 声明它不是一个框架,而是一个部署工具,因此开发者只需在代码中调用 ClaudiaJS,而无需改变代码结构。ClaudiaJS 建立在 AWS SDK 和 AWS CLI 之上。它标识了三种类型的 JavaScript 库:

  • 命令行库

  • API 构建库

  • Bot 构建库

命令行库

第一个 JavaScript 库是一个命令行工具或库。该命令行工具帮助部署、更新、回滚、打包、调用或测试以及销毁 Lambda 函数,它还与 AWS API Gateway 无缝集成。它使用标准的 npm 打包惯例,这意味着你可以在不修改代码结构的情况下直接调用它。所以,ClaudiaJS 的命令行库提供的有趣功能如下:

  • claudia create : 这个命令将在 AWS 门户上创建一个函数和相关的安全角色。

  • claudia update : 这个命令将通过部署新版本的函数来更新功能,并更新相关的 API。

  • claudia test-lambda: 这个命令将执行 Lambda 函数。

  • claudia set-version: 这个命令将把 Lambda API 阶段指向最新的部署版本。

  • claudia add-scheduled-event: 这个命令可以为 Lambda 函数添加定期执行的计划事件,通过这个命令,我们可以保持 Lambda 函数处于“热”状态。

  • claudia destroy: 这个命令将销毁函数及其相关的 API 和安全角色。

API 构建库

第二种类型的库是 API 构建库。它是 ClaudiaJS 库的扩展,帮助设置 AWS API Gateway 端点。它还帮助将多个 API 网关路由到单一的 Lambda 函数,并自动为这些端点启用 CORS。

Bot 构建库

这个库是 ClaudiaJS 提供的最有趣的库之一。这个库可以帮助在几分钟内创建不同类型的机器人。它提供开箱即用的功能,能够与 Facebook Messenger、Telegram 和 Skype 集成。使用 ClaudiaJS 的机器人库,设置一个机器人相当简单。

总结来说,ClaudiaJS 在使用 AWS 云服务提供商时非常好用。它也支持 Node.js。以下信息框中提供的文档链接是最新的,文档详细解释了每一个 CLI 命令。提供了很多教程,涵盖从简单开发到高级任务。机器人库是 ClaudiaJS 提供的最棒的功能之一。然而,它不支持多个无服务器提供商,也不支持多语言。

更多关于 Claudia.JS 的信息可以在以下链接找到:

claudiajs.com

Apex

Apex是另一个基于 Go 构建的无服务器框架,用于管理 AWS Lambda 函数。它是一个开源框架,使用 Terraform 来启动资源,使得执行速度更快。该框架提供的功能包括部署、测试函数、回滚部署、查看指标和跟踪日志。

尽管它不支持本地调用函数,但它支持多种语言,如 Node.js、Python、Java、Rust 和 Go。我们可以通过 Apex 创建各种环境。它有很好的文档,帮助你快速上手使用该框架。然而,Apex 目前只支持 AWS Lambda。

更多关于 Apex 的信息可以在以下链接找到:

apex.run/#function-hooks

Zappa

如果你决定用 Python 编写函数,那么你可以使用 Zappa 来部署它们。Zappa是一个 CLI/命令行框架,并且是开源的。Zappa 目前支持 Python WSGI 应用,基本上是 Flask 和 Django 应用。它可以部署宏应用和微应用。Zappa 拥有各种功能,如能够将 API 等函数部署到 AWS Lambda 和 AWS API Gateway。它还可以配置 AWS 事件源。

一旦部署,我们还可以通过 Zappa 调用函数。它可以从 AWS 获取或跟踪日志。它还支持回滚到之前的版本。我们可以设置多阶段部署(通过stage,指的是多个环境部署,如devqauatprod)。

Zappa 还具有一个很酷的功能,可以保持 Lambda 函数常驻。这可以提高性能并在一定程度上减少延迟。它允许我们安排部署,这意味着我们可以在一天的早些时候设置部署,以免干扰常规流量。它还具备撤销部署和从 CloudWatch 清除日志的能力。我们还可以使用它打包 Lambda 函数以供未来部署。部署后的状态也可以通过 Zappa 进行检查。Zappa 允许我们将 Lambda 函数部署到 AWS 的任何区域。我们来看看 Zappa 的一些功能:

$ pip install zappa 
  • Zappa init: init 命令将设置部署配置。它应该会自动检测到 Flask/Django 应用,并在项目目录中创建一个名为 zappa_settings.json 的 JSON 文件:
$ zappa init 
  • 打包和部署: 一旦设置好配置,我们可以使用以下命令打包和部署应用程序。默认情况下,它使用生产环境,但我们可以创建多个不同的环境:
 $ zappa deploy production

Zappa 是一个很棒的框架,但它也有一些缺点,比如不支持其他云服务提供商,如 Azure、Google 和 OpenWhisk。它仅支持基于 Python-WSGI 的应用程序,不支持其他语言,如 Node.js。

你可以在以下链接找到更多关于 Zappa 框架的信息:

www.zappa.io/

Serverless Framework

Serverless Framework 是构建无服务器架构最受欢迎的框架之一。它是一个开源命令行工具,在 GitHub 上大约有 23,000 个 stars。它还有一个企业版,帮助设置模板并提供支持。许多公司,如 EA、可口可乐、Expedia 和路透社,都在使用这个框架。它支持很多云服务提供商,如 AWS、Azure、Google、OpenWhisk、Kubeless、Oracle Fn 等。它有一个文档非常完善的用户指南,包含大量示例,帮助你快速上手。它支持多种语言,如 Node.js、Python、Java、Scala、C#、Go、F#、Groovy、Kotlin、PHP 和 Swift。

它支持无服务器架构的生命周期,可以进行构建、部署、更新和删除。它支持功能分组,便于在大型项目中管理代码、流程和资源,并且对 CD/CI 提供了相当好的支持。与其他框架相比,它拥有更强大的社区支持。它提供了大量插件来支持框架功能,并且有很多博客可以帮助我们建立使用框架的最佳实践。它还提供了支持论坛和 Slack 聊天室来解决问题。它支持很多功能,如部署函数和事件、调用函数、跟踪日志、集成测试以及为将来部署进行打包。让我们更详细地了解一下 Serverless Framework 的功能。

框架功能

Serverless Framework 中有许多功能,尽管它们会因云提供商而异。我将列出并描述一些更重要和更常见的功能。

服务和部署

服务是我们定义函数、触发这些函数的事件,以及为函数执行所需的任何基础设施资源的项目。它们被收集到一个文件中,这个文件叫做serverless.yml

eg. 
 myServerlessService/
        serverless.yml

当我们开始使用 Serverless Framework 进行部署时,我们将使用一个单一的服务。但随着应用的增长,建议您使用多个服务,如以下代码所示:

 users/
        serverless.yml # Contains 4 functions  
 posts/
         serverless.yml # Contains 4 functions

拥有多个服务可以隔离要使用的基础设施资源。但它也有一个缺点,即目前每个服务都会在 API Gateway 上创建一个独立的 REST API。这是 API Gateway 的一个限制。但有一个解决方法,我们将在未来的章节中讨论。

要创建服务,我们必须使用create命令,并且必须传递您希望编写服务的运行时语言。我们还可以提供路径,如下例所示:

$ serverless create --template <runtimes> --path myService

Serverless Framework 的主要目的是将函数、事件和基础设施资源部署到远程云端,且无需太多麻烦,这是通过deploy插件完成的。这个deploy插件提供了各种功能。让我们来看其中的一些:

  • 部署到不同的阶段和区域:

    $ serverless deploy --stage production --region us-east-1

  • 从服务中部署单个函数:

    $ serverless deploy function <function_name>

  • 部署包到云端:

    $ serverless deploy --package <path to package>

这个deploy插件的工作方式如下:

  • 框架将目标 AWS Lambda 函数打包成.zip文件

  • 框架获取已上传的函数.zip文件的哈希值,并将其与本地.zip文件的哈希值进行比较

  • 如果两个哈希值相同,框架将终止

  • .zip文件使用与之前函数相同的名称上传到您的 S3 存储桶,这就是它指向的 CloudFormation 堆栈

函数和事件

函数是定义在服务中的属性,它们在 serverless.yml 文件中定义,因此我们为函数命名并提供 handler 属性,指向函数文件,该文件可以是 Node.js 或 Python。我们可以在该属性中添加多个函数。函数可以继承提供者的属性,也可以在函数级别定义属性。这些函数属性会根据云提供商的不同而有所变化,如以下代码所示:

# serverless.yml
service: myService
provider:
  name: aws
  runtime: nodejs6.10
  memorySize: 512 # will be inherited by all functions
functions:
  usersAdd:
    handler: handler.userAdd
    description: optional description for your function
userModify:
    handler: handler.userModify
userDelete:
    handler: handler.userDelete
    memorySize: 256 # function specific

如果为每个函数创建一个单独的文件,可以将函数列为数组:

 # serverless.yml

 functions: 
     - ${file(../user-functions.yml)}
   - ${file(../post-functions.yml)}
 # user-functions.yml 
addUser: 
     handler: handler.user 
deleteUser: 
      handler: handler.user

可以在服务中的函数内添加环境对象属性,且它应该是键值对。同时,函数特定的环境变量会覆盖提供者特定的环境变量:

 # serverless.yml 
service: service-name 
provider: aws 

 functions: 
   hello: 
       handler: handler.hello 
       environment: 
           TABLE_NAME: tableName

事件是触发函数的操作,例如 S3 存储桶上传。Serverless 框架支持多种事件,但它们根据云提供商不同而有所变化。我们可以为单个函数定义多个事件,如以下代码所示:

events:
 - http:
    path: handler
    method: get

Serverless 框架为 AWS Lambda 提供的事件类型如下链接所示:

serverless.com/framework/docs/providers/aws/events/

变量与插件

变量是在运行 Serverless 框架命令时,可以传递给 serverless.yml 配置值的值。它们需要通过 ${} 括号传递引用值,但你可以在属性值中使用变量,而不是在属性键中。以下代码展示了如何在 serverless.yml 文件中添加这些变量:

provider:  
    name: aws 
     stage: ${opt:stage, 'dev'}

在以下语句中,我们将参数传递给 CLI,stage 值将被填充到 serverless.yml 文件中:

$ serverless deploy --stage qa

变量可以作为引用属性递归使用——也就是说,我们可以将多个值和变量来源结合起来,如以下环境变量所示:

 environment: SERV_SECRET: ${file(../config.${self:provider.stage}.json):CREDS} 

我们也可以引用环境变量(如以下代码所示),但是将敏感数据添加到环境变量中是不安全的,因为它们可能会通过构建日志或 Serverless CloudFormation 模板被访问:

functions: 
    myfunction: 
        name: ${env:FUNC_PREFIX}-myfunction 
        handler: handler.myfunction

我们来看看几个实现这一点的教程。确保你已安装并正常运行最新版本的 Serverless 框架:

  1. 使用 Serverless AWS 模板创建一个简单的 hello 项目,然后在你喜欢的编辑器中打开该项目:
 $ serverless create --template aws-nodejs --path env-variable-service 
  1. 替换 serverless.yml 文件和 handler,如以下代码所示。在这里,我们通过 MY_VAR 的环境变量名添加一个环境变量,并在 handler 中显示该环境变量的值:
  $ cat serverless.yml
service: env-variable-service
# You can pin your service to only deploy with a specific Serverless version
 # Check out our docs for more details
 # frameworkVersion: "=X.X.X"
provider:
   name: aws
   runtime: nodejs6.10
# you can define service wide environment variables here
   environment:
     MY_VAR: Lion
  functions:
     hello:
       handler: handler.hello
 $ less handler.js
'use strict';
module.exports.hello = (event, context, callback) => {
const response = {
  statusCode: 200,
  body: JSON.stringify({
    message: `my favourite animal is ${process.env.MY_VAR}`,
    input: event,
  }),
};
callback(null, response);
};
  1. 让我们在本地调用并查看结果。如果你查看消息部分,你可以看到我们定义的环境变量的值,如以下代码所示:
 $ serverless invoke local -f hello
 {
 "statusCode": 200,
 "body": "{\"message\":\"my favourite animal is Lion\",\"input\":\"\"}"
 }

正如我们之前讨论的,对于非敏感数据,将变量添加到serverless.yml文件应该没问题,但我们如何将敏感数据(如数据库连接)添加到环境变量中呢?让我们看看实现这一目标所需的步骤:

  1. env-variable-service文件夹中创建一个新文件,命名为serverless.env.yml。然后按照以下代码的示例添加相应的内容。在这里,我们根据环境创建一个密钥变量:
 $ less serverless.env.yml
 dev:
   MYSECRET_VAR: 'It is at secret den'

  1. 让我们在serverless.yml文件中再添加一个环境变量,但这次它的值将从文件中获取,因此你需要添加高亮显示的那一行作为环境变量。这样,Serverless Framework 将读取文件并将其引用到特定的环境中:
# serverless.yml
 # you can define service wide environment variables here
environment:
   MY_VAR: Lion
   MYSECRET_VAR: ${file(./serverless.env.yml):dev.MYSECRET_VAR}
  1. 让我们更改处理程序的响应,通过消息显示密钥。理想情况下,我们应该在屏幕上显示密钥,但为了本教程,我将手动进行。所以让我们将消息体替换为以下代码中显示的内容:
 # handler.js
message: `my favourite animal is ${process.env.MY_VAR} and ${process.env.MYSECRET_VAR}`,
  1. 本地调用函数并查看输出:
 $ serverless invoke local -f hello
 {
 "statusCode": 200,
 "body": "{\"message\":\"my favourite animal is Lion and It is at secret den\",\"input\":\"\"}"
 }

最后,正如我之前所说,我们可以为每个部署阶段设置多个环境变量,如devsituatprod。以下步骤展示了如何添加这些环境变量:

  1. 让我们为prod添加一个环境变量到serverless.env.yml文件中。然后,我们可以在serverless.yml文件中动态使用它们,如下所示:
 $ less serverless.env.yml
dev:
   MYSECRET_VAR: 'It is at secret dev den'
prod:
   MYSECRET_VAR: 'It is at secret prod den'
  1. 现在,我们需要对serverless.yml文件进行修改,动态地根据调用或部署函数时设置的阶段来获取环境变量,这就是将MYSECRET_VAR行替换为以下行:
 #serverless.yml
MYSECRET_VAR: ${file(./serverless.env.yml):${opt:stage}.MYSECRET_VAR}
  1. 现在我们将本地调用该函数,并查看不同阶段的输出:
 $ serverless invoke local -f hello -s dev
 {
 "statusCode": 200,
 "body": "{\"message\":\"my favourite animal is Lion and It is at secret dev den\",\"input\":\"\"}"
 }

 $ serverless invoke local -f hello -s prod
 {
 "statusCode": 200,
 "body": "{\"message\":\"my favourite animal is Lion and It is at secret Prod den\",\"input\":\"\"}"
 }

我已经将前面的教程上传到以下 GitHub 仓库,你可以随时使用:

github.com/shzshi/env-variable-service.git

插件是提供扩展现有 Serverless Framework CLI 命令的自定义 JavaScript 代码。框架本身就是一组核心提供的插件。我们可以构建自己的自定义插件,Serverless Framework 也提供了该插件的文档。可以使用以下代码安装插件:

$ npm install --save custom-serverless-plugin

我们还需要在serverless服务中调用它们,使用以下代码:

serverless.yml file 
 plugins:
     - custom-serverless-plugin

可以通过以下链接查看现有的 Serverless 插件列表:

github.com/serverless/plugins

资源

当我们创建 Lambda 函数时,它们可能依赖于多种不同类型的基础设施资源,如 AWS、DynamoDB 或 AWS S3,因此我们可以在 serverless.yml 文件中定义这些资源并进行部署。当我们添加这些资源时,它们将被添加到 serverless.yml 文件中,并且在部署时,它们会被添加到 CloudFormation 堆栈中并在 serverless deploy 时执行。我们可以通过以下示例查看这些资源是如何定义的:

resources:
  Resources:
    NewResource:
       Type: AWS::S3::Bucket
       Properties:
         BucketName: my-s3-bucket

您可以参考以下链接,了解更多资源详情,并查看仅使用 AWS Lambda 时可用的资源:serverless.com/framework/docs/providers/aws/guide/resources/

让我们看一个使用 AWS Lambda 的 Serverless Framework 简单示例。

您将需要以下先决条件:

  • 需要创建一个免费的 AWS 账户。

  • 本地机器上必须安装 Node.js 4.0 或更高版本。

  • AWS CLI 也可以安装,但这是可选的。

设置 AWS 访问密钥。

请按照以下步骤操作:

  1. 登录到 AWS 账户并进入 IAM(身份与访问管理)页面。

  2. 点击左侧栏的用户(Users),然后点击“添加用户”按钮并添加用户名 adm-serverless。接着启用编程访问(programmatic access),勾选复选框。然后点击下一步:权限按钮(Next:Permissions)。

  3. 在此页面上,选择直接附加现有策略,搜索并选择 AdministratorAccess 复选框,然后点击下一步:审核。

  4. 现在检查一切是否正常,然后点击“创建用户”。这将创建一个用户并显示我们的访问密钥 ID 和秘密访问密钥。暂时复制并存储这些密钥。

  5. 现在我们已经获得了密钥,我们将其作为环境变量导出,以便框架能够访问并执行其所需的功能:

$ export AWS_ACCESS_KEY_ID=<access-key-id>
$ export AWS_SECRET_ACCESS_KEY=<secret-key-id>

安装 Serverless Framework。

请按照以下步骤操作:

  1. 从 nodejs.org/en/download/ 安装 Node.js 4.0 或更高版本。安装完成后,我们可以通过以下命令验证安装:
 $ node --version
  1. 现在我们需要通过以下命令全局安装 Serverless Framework:
 $ npm install -g serverless
  1. 安装成功后,我们可以通过以下命令验证安装。它将显示所有框架命令和文档:
$ serverless
  1. 我们还可以通过以下命令查看已安装的 Serverless Framework 版本:
$ serverless --version

Lambda 服务和函数部署。

在接下来的步骤中,我们将创建一个简单的 Node.js 服务和 Lambda 函数,然后部署和调用它们:

  1. 使用 Serverless Framework 创建一个新的服务,选择 Node.js 模板。我们需要确保名称唯一,并可以选择性地添加服务路径。此命令将创建两个文件—handler.jsserverless.yml
$ serverless create --template aws-nodejs --path my-serverless-service
Serverless: Generating boilerplate...
 Serverless: Generating boilerplate in "/Users/shashi/Documents/packt/chapter2/serverless/my-serverless-service"
 _______ __
 | _ .-----.----.--.--.-----.----| .-----.-----.-----.
 | |___| -__| _| | | -__| _| | -__|__ --|__ --|
 |____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
 | | | The Serverless Application Framework
 | | serverless.com, v1.26.1
 -------'
Serverless: Successfully generated boilerplate for template: "aws-nodejs"

服务是框架的组织单元。它可以被视为一个项目文件。在这里,我们可以定义函数、触发这些函数的事件以及我们将使用的函数资源。这些内容都被放入一个文件serverless.yml中。在这个.yaml文件中,我们定义了名为my-serverless-service的服务。然后我们定义了提供者;正如我之前提到的,Serverless Framework 支持许多其他云服务提供商。我们可以在这个标签中列出提供者的详细信息,并且还可以提到运行时环境,在我们的案例中是 Node.js。运行时环境会根据我们编写函数所使用的语言而变化。我们可以定义部署环境或阶段—在我们的案例中是dev—以及相应的区域。接着,在functions部分,我们定义了函数名称—在我们的案例中是hello—它有一个名为handler的属性,这个处理程序将调用handler.js文件。我们还可以定义内存大小。接下来,我添加了一个 HTTP 事件,它除了配置 Lambda 函数外,还配置了 AWS API Gateway。它将为处理程序创建并提供一个端点。所以,使用一个脚本,我们可以配置 Lambda 函数和 API 端点。我们可以定义其他各种属性和参数;我们将在下一章中详细研究这些内容。

请确保.yaml文件正确缩进,否则它们将无法运行。你也可以使用我在以下 GitHub 链接中提供的文件:

github.com/shzshi/my-serverless-service.git

$ cat serverless.yml 
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
# docs.serverless.com 
#
# Happy Coding!
service: my-serverless-service
# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: us-east-1
functions:
hello:
handler: handler.hello
memorySize: 128
events:
- http:
path: handler
method: get

handler.js代码段是Hello, World!的 Node.js Lambda 函数,在serverless.yml文件中引用。它是一个相当简单的函数,执行时将显示消息My Serverless World,如下所示:

$ cat handler.js
'use strict';
module.exports.hello = (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'My Serverless World',
input: event,
}),
};
callback(null, response);
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};

本地调用

每次将代码推送到 AWS Lambda 并进行测试既昂贵又耗时。所以,通过 Serverless Framework,我们可以在本地调用或测试函数,然后将其部署到云端。我们可以将此作为持续部署管道的一部分,在这里我们可以为dev阶段部署设置本地调用,设置自动化测试,然后将它们进一步推进管道,进行远程部署和测试。以下命令用于本地调用函数:

$ serverless invoke local --function hello
 {
 "statusCode": 200,
 "body": "{\"message\":\"My Serverless World\",\"input\":\"\"}"
 }

本地部署和调用

既然我们现在能够成功地在本地调用和测试函数,接下来我们应该可以将其部署并进行远程测试。首先,我们需要确保已将访问密钥和秘密访问密钥作为环境变量获取并导出,代码如下所示:

$ export AWS_ACCESS_KEY_ID=<access-key-id>
$ export AWS_SECRET_ACCESS_KEY=<secret-key-id> 

现在我通过一个简单的 deploy 命令将功能和 API 部署到 AWS 云中。在后台,部署过程将创建一个 .serverless 文件夹,包含 CloudFormation JSON 模板。无服务器代码被打包成 .zip 文件以及一个无服务器状态 JSON 文件。如果我们查看 create-stack JSON 模板,Serverless Framework 将在 AWS 云上创建一个 S3 存储桶,并将功能和 API 包部署到该存储桶中,使用 CloudFormation 模板 JSON 文件。它还会以 JSON 文件的形式保持部署状态。成功部署后,将提供一个 API 端点,该端点与 AWS Lambda 功能绑定,并创建一个如提供商中提到的服务:

$ serverless deploy

 Serverless: Packaging service...
 Serverless: Excluding development dependencies...
 Serverless: Creating Stack...
 Serverless: Checking Stack create progress...
 .....
 Serverless: Stack create finished...
 Serverless: Uploading CloudFormation file to S3...
 Serverless: Uploading artifacts...
 Serverless: Uploading service .zip file to S3 (417 B)...
 Serverless: Validating template...
 Serverless: Updating Stack...
 Serverless: Checking Stack update progress...
 ..............................
 Serverless: Stack update finished...
 Service Information
 service: my-serverless-service
 stage: dev
 region: us-east-1
 stack: my-serverless-service-dev
 api keys:
 None
 endpoints:
 GET - https://kn6esoolgi.execute-api.us-east-1.amazonaws.com/dev/handler
 functions:
 hello: my-serverless-service-dev-hello

部署后,我们将进入 AWS 门户,查看该功能是否已部署。然后我们将通过门户调用它,并再次通过无服务器 CLI 调用远程功能,使用以下步骤:

  1. 登录到 AWS 门户,然后选择已部署功能和 API 的正确区域。

  2. 选择服务为 Lambda。Lambda 门户页面将显示已部署的功能名称。使用单选按钮选择你要测试的功能,然后转到标记为“操作”的下拉菜单并选择“测试”选项。窗口将弹出配置事件。你可以添加自己的事件或保持默认设置,然后点击“保存”。事件将被保存,页面将重定向到功能页面。现在点击“测试”按钮。功能将被执行,执行状态和结果将显示出来。

如前所述,我们也可以在本地调用远程功能。现在我们将查看远程 Lambda 功能的各种命令:

要调用该功能并获取日志,请使用以下命令:

 $ serverless invoke -f hello -l
 {
 "statusCode": 200,
 "body": "{\"message\":\"My Serverless World\",\"input\":{}}"
 }
 --------------------------------------------------------------------
 START RequestId: c4bdc7f8-60e1-11e8-9846-f5267af11144 Version: $LATEST
 END RequestId: c4bdc7f8-60e1-11e8-9846-f5267af11144
 REPORT RequestId: c4bdc7f8-60e1-11e8-9846-f5267af11144 Duration: 44.37 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 20 MB

若只需获取上次调用的日志,请输入以下代码。我们可以在一个单独的控制台中测试其功能:

$ serverless logs -f hello -t
START RequestId: 75139847-60dc-11e8-8c0b-4f4996ceffb3 Version: $LATEST
 END RequestId: 75139847-60dc-11e8-8c0b-4f4996ceffb3
 REPORT RequestId: 75139847-60dc-11e8-8c0b-4f4996ceffb3 Duration: 9.04 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 20 MB
START RequestId: 675574ec-60de-11e8-bbd5-8bf1c1e7f05c Version: $LATEST
 END RequestId: 675574ec-60de-11e8-bbd5-8bf1c1e7f05c
 REPORT RequestId: 675574ec-60de-11e8-bbd5-8bf1c1e7f05c Duration: 18.15 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 20 MB

该命令将从 Lambda 中撤销功能的部署,并移除 S3 存储桶中的包详情。一旦serverless remove成功运行(如以下代码所示),你可以登录 AWS 门户,检查 S3 存储桶和 Lambda 功能的具体区域——它应该已经被移除:

$ serverless remove
Serverless: Getting all objects in S3 bucket...
 Serverless: Removing objects in S3 bucket...
 Serverless: Removing Stack...
 Serverless: Checking Stack removal progress...
 .........
 Serverless: Stack removal finished...

总结

在本章中,我们学习了不同的框架,例如 ClaudiaJS、Zappa 和 Apex。我们还查看了一些使用它们的示例,但我们主要深入探讨了 Serverless Framework。在本书中的大部分教程中,我们将广泛使用 Serverless Framework,因为它比其他框架表现得更好,因为它具有更好的社区支持和对多个云提供商的支持。这意味着你不会像使用某些其他框架时那样被绑定到特定供应商。它还拥有大量的插件,支持不同类型的云服务,良好的博客支持,最后,还有一些非常适合轻松部署到不同云提供商的优良功能。

第三章:将 DevOps 应用到 AWS Lambda 应用程序

我们简要回顾一下关于 AWS Lambda 函数的内容。亚马逊网络服务是第一个推出无服务器计算模块 Lambda 的网络服务平台,Lambda 函数是用 Lambda 函数 编写的。Lambda 函数是无状态的,并且与底层基础设施没有依赖关系。Lambda 函数是响应事件执行的,这些事件可能是一个 HTTP 请求、S3 桶中的数据变化、DynamoDB 表的变化,或者 Kinesis 或 SNS 的变化。Lambda 函数会在事件发生时快速复制,并且在事件数量减少时会缩减规模。

在本章中,我们将介绍部署 Lambda 函数的各种方法,探讨如何轻松地将其部署到多个环境,进行单元测试、系统测试和集成测试。我们还将学习各种部署最佳实践,并通过几个示例来演示这些最佳实践。我们将看到如何管理 AWS Lambda 日志,并将其移到 ELK 堆栈中。

在本章中,我们将探讨以下主题:

  • 手动部署 Lambda 函数

  • 使用 DevOps 的 AWS Lambda

  • 使用 CodeStar 的无服务器架构

  • 使用 AWS Lambda 进行蓝绿部署

  • 使用 Serverless Framework 的 GitHub 和 Jenkins 管道

  • 为无服务器应用程序设置 Jenkins

  • 对已部署应用程序进行单元测试

  • 将 CloudWatch 与 ELK 集成

我将创建一个简单的应用程序,这是我们日常工作中使用的应用程序。该应用程序是一个缩略图创建器,使用的是 Node.js 应用程序,涉及两个 S3 桶。一个桶用于上传实际的图像,另一个用于存放缩略图。当一张图片上传到图像桶时,会触发一个事件,调用一个函数来调整图片大小并将其上传到缩略图桶中。我们首先看看如何手动执行这一系列事件,然后学习如何通过自动化部署过程来简化这个过程。在 DevOps 相关部分,我们将讨论如何搭建开发环境、自动化测试与部署、应用 CI/CD 管道、日志记录和监控。

手动部署 Lambda 函数

我们将在此处使用的 Node.js Lambda 应用程序已经是 AWS 教程的一部分。我们将学习如何通过 AWS 门户创建、部署和执行 Lambda 应用程序。本教程的前提是你拥有一个 AWS 账户;在本章中我们将使用 AWS 免费订阅。下一步是设置 AWS CLI。

你可以通过以下链接创建 AWS 免费账户并设置 AWS CLI:

portal.aws.amazon.com/billing/signup#/start

docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html

请按照以下步骤操作:

  1. 在 AWS 账户和 CLI 设置好后,登录 AWS 控制台 (aws.amazon.com/console/),然后我们将通过登录您的 AWS 账户创建名为 adminuser 的 IAM 用户,您可以点击 IAM 链接或通过服务搜索该链接:

  1. 然后,我们点击左侧的 Users 链接,打开一个新页面。接着点击 Add User,添加一个名为 adminuser 的用户,并选择两种访问类型:编程访问(Programmatic access)和 AWS 管理控制台访问(AWS Management Console access)。在控制台的密码字段中,输入自定义密码,取消勾选“需要重置密码”复选框,然后点击 Next:Permissions。接下来,我们点击 Create Group 按钮来创建一个新组。我们将该组命名为 administrators。然后,勾选 AdminstrativeAccess 复选框,为该组提供完全访问权限,然后点击 Create Group。现在,我们已经创建了一个组,点击 Next:Review。接下来,我们将审核该用户,确保该用户已被添加到管理员组中。然后点击 Create User 按钮。一旦用户创建完成,我们应该能在列表中看到该用户,如下图所示:

我们为教程创建了一个具有管理员权限的用户,但在实际操作中,出于安全考虑,角色和策略会更加受限。

  1. 我们需要在 AWS S3 中创建两个桶。这些桶需要通过 adminuser 登录创建,因此我们需要使用刚才创建的新用户登录到 AWS 控制台。点击 adminuser 并选择 Security credentials 选项卡。接下来,复制控制台 URL 以进行登录,然后在新的浏览器标签页中打开它。输入我们刚才创建的新用户的用户名和密码,点击 Sign In 登录。一旦登录,搜索 AWS 服务中的 S3。然后进入 S3 控制台管理,点击 Create bucket 按钮,如下图所示:

默认情况下,我们将添加一个唯一的桶和区域名称,区域为美国东部。两个桶的名称应分别为 SourceSourceResizedSource 是占位符名称,应替换为实际的桶名称——例如,my-image-bucket76my-image-bucket76resized因此,my-image-bucket76 将是源桶,而 my-image-bucket76resized 将是目标桶,如下图所示:

桶名称必须唯一,因为 AWS 只允许使用全局唯一的桶名称。

  1. 一旦两个存储桶都成功创建,我们可以上传一张图片,让 Lambda 函数进行调整大小,并将其推送到调整后的存储桶。让我们将一张 JPG 图片上传到 my-source-bucket76 存储桶中。点击存储桶名称,然后上传一张图片到这个存储桶。这样会将您重定向到存储桶页面。点击上传按钮,然后会弹出一个窗口。接着,选择“添加文件”以浏览本地目录中的图片文件,并将图片上传到 S3 存储桶中。

  2. 下一步是创建一个 Lambda 函数并手动运行它。在这里,我们首先需要按照三个步骤创建部署包,然后创建执行角色(IAM 角色)和 Lambda 函数并进行测试。部署包是一个 ZIP 文件,包含 Lambda 函数及其依赖项:

    • 让我们创建一个部署包。我将使用 Node.js 作为此实践应用的语言,但我们也可以使用 Java 和 Python(这取决于开发人员的偏好)。

    • 创建此部署包的前提是您的本地环境中安装了 Node.js 6.0 或更高版本(nodejs.org/en/download/)。您还应确保已安装 npm。

    • 然后按照以下步骤进行操作。我们将下载用于 Node.js Lambda 函数的图像调整大小的 npm 库,使用以下两个命令中的第一个。第二个命令将下载所需的库:

$ mkdir tutorial1; cd tutorial1 $ npm install async gm
    • 打开您最喜欢的编辑器,并将以下脚本复制到名为 CreateThumbnails.js 的文件中:
// dependencies
var async = require('async');
var AWS = require('aws-sdk');
var gm = require('gm')
            .subClass({ imageMagick: true }); // Enable ImageMagick integration.
var util = require('util');
// constants
var MAX_WIDTH  = 100;
var MAX_HEIGHT = 100;
// get reference to S3 client
var s3 = new AWS.S3();
exports.handler = function(event, context, callback) {
    // Read options from the event.
    console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
    var srcBucket = event.Records[0].s3.bucket.name;
    // Object key may have spaces or unicode non-ASCII characters.
    var srcKey    =
    decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); 
    var dstBucket = srcBucket + "resized";
    var dstKey    = "resized-" + srcKey;

您可以在 gist.github.com/shzshi/6e1cf435a4c1aa979e3a9a243c13c44a 找到 CreateThumbnails.js 的要点。

    • 作为基本检查,请验证源和目标是否是不同的存储桶:
    if (srcBucket == dstBucket) {
       callback("Source and destination buckets are the same.");
        return;
    }
    • 推断图片类型:
var typeMatch = srcKey.match(/\.([^.]*)$/);
    if (!typeMatch) {
        callback("Could not determine the image type.");
        return;
    }
    var imageType = typeMatch[1];
    if (imageType != "jpg" && imageType != "png") {
        callback('Unsupported image type: ${imageType}');
        return;
    }
    • 从 S3 下载图片,进行转换,并上传到另一个 S3 存储桶:
async.waterfall([
        function download(next) {
            // Download the image from S3 into a buffer.
            s3.getObject({
                    Bucket: srcBucket,
                    Key: srcKey
                },
                next);
            },
        function transform(response, next) {
            gm(response.Body).size(function(err, size) {
    • 推断缩放因子,以避免不自然地拉伸图片:
var scalingFactor = Math.min(
                    MAX_WIDTH / size.width,
                    MAX_HEIGHT / size.height
                );
                var width  = scalingFactor * size.width;
                var height = scalingFactor * size.height;
    • 在内存中转换图片缓冲区:
this.resize(width, height)
                    .toBuffer(imageType, function(err, buffer) {
                        if (err) {
                            next(err);
                        } else {
                            next(null, response.ContentType, buffer);
                        }
                    });
            });
        },
        function upload(contentType, data, next) {
    • 将转换后的图片流式传输到另一个 S3 存储桶:
s3.putObject({
                    Bucket: dstBucket,
                    Key: dstKey,
                    Body: data,
                    ContentType: contentType
                },
                next);
            }
        ], function (err) {
            if (err) {
                console.error(
                    'Unable to resize ' + srcBucket + '/' + srcKey +
                    ' and upload to ' + dstBucket + '/' + dstKey +
                    ' due to an error: ' + err
                );
            } else {
                console.log(
                    'Successfully resized ' + srcBucket + '/' + srcKey
+
                    ' and uploaded to ' + dstBucket + '/' + dstKey
                );
            }
            callback(null, "message");
        }
    );
};

现在我们应该能在 tutorials1 文件夹中看到两个项目,即 CreateThumbnail.jsnode_modules 文件夹。我们将所有这些文件压缩成一个名为 tutorialsimg.zip 的文件。这将是我们的 Lambda 函数部署包:

$ cd tutorials1
$ zip -r ../tutorialsimg.zip *
  1. 接下来,我们将在 IAM 中创建 Lambda 函数的执行角色。登录到 AWS 控制台,搜索 IAM 服务,然后进入 IAM 并点击“角色”按钮。点击“创建角色”,选择“AWS 服务”,然后选择 Lambda 作为服务。然后点击“下一步:权限”按钮。接着在策略类型框中搜索 AWSLambda,并勾选 AWSLambdaExecute 复选框。然后搜索 AmazonS3FullAccess 并选择它。然后点击“下一步:审查”按钮。接着,在创建角色页面中,添加角色名称 myLambdaRole,添加描述,然后点击“创建角色”。现在我们拥有了包含可以用于执行 Lambda 函数的策略的角色,以便对 S3 存储桶的内容进行更改。

  2. 下一步是在 AWS Lambda 门户上部署 Lambda 函数和节点模块。我们首先进入 AWS 控制台首页,在服务下搜索 Lambda。我们将被重定向到 Lambda 首页。点击“创建函数”,然后在函数信息部分选择“从头开始编写”。我们将函数命名为myfirstLambdafunction,运行时选择Node.js 6.10。选择现有角色名称为 myLambdaRole 并点击“创建函数”。现在我们将被重定向到 Lambda 函数设计页面。我们将把 Lambda 函数上传到门户。稍微向下滚动,进入“函数代码”部分,然后选择代码输入类型为上传 .ZIP 文件,运行时为 Node.js 6.10,并在处理程序字段中输入文本CreateThumbnail.handler。现在点击上传按钮,选择名为 tutorialsimg.zip 的文件并上传。当包成功上传后,我们应该能够在函数编辑器中看到 CreateThumbnail.js 文件和 node_modules 文件夹,如下截图所示:

  1. 现在,函数和节点模块已经上传,我们将创建一个事件来触发该函数。如果我们滚动到屏幕的右上角,我们将看到一个下拉菜单出现,我们可以用它来添加事件。选择“配置测试事件”。 一个弹出框将出现。为事件命名为myThumbnailEvent,并在文本字段中添加以下列出的 JSON 文件。确保将my-source-bucket76替换为你的源存储桶名称,将Baby.jpg替换为你的图像名称。然后继续点击保存:

    你可以在gist.github.com/shzshi/7a498513ae43b6572c219843bbba277d找到事件 JSON 文件。

{
  "Records": [
    {
      "eventVersion": "2.0",
      "eventSource": "aws:s3",
      "awsRegion": "us-west-2",
      "eventTime": "1970-01-01T00:00:00.000Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "AIDAJDPLRKLG7UEXAMPLE"
      },
      "requestParameters": {
        "sourceIPAddress": "127.0.0.1"
      },
      "responseElements": {
        "x-amz-request-id": "C3D13FE58DE4C810",
        "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "***my-source-bucket76***",
          "ownerIdentity": {
            "principalId": "A3NL1KOZZKExample"
          },
          "arn": "arn:aws:s3:::***my-source-bucket76***"
        },
        "object": {
          "key": "***Baby.jpg***",
          "size": 1024,
          "eTag": "d41d8cd98f00b204e9800998ecf8427e",
          "versionId": "096fKKXTRTtl3on89fVO.nfljtsv6qko"
        }
      }
    }
  ]
}
  1. 现在我们已经部署了这个功能,并创建了 S3 存储桶和事件。接下来让我们调用这个功能,看看源存储桶中的图像是否被调整大小并推送到调整大小后的 S3 存储桶。为此,请点击“测试”。我们应该能在日志中看到该功能已经成功执行(如下所示的日志文本)。如果你刷新调整大小后的 S3 存储桶,你应该能看到已调整大小的图像文件。你可以直接下载调整大小后的文件,查看调整大小是否成功。我们还可以添加 S3 上传触发器,在任何图像文件上传到源 S3 存储桶时自动触发 CreateThumbnail 函数。

eTag: 'd41d8cd98f00b204e9800998ecf8427e',versionId: '096fKKXTRTtl3on89fVO.nfljtsv6qko' } } } ] } 2018-06-14T21:07:25.469Z ea822830-7016-11e8-b407-9514918aacd8

成功调整大小了 my-source-bucket76/Baby.jpg 并上传至 my-source-bucket76resized/resized-Baby.jpg END RequestId: ea822830-7016-11e8-b407-9514918aacd8

在这个练习中,我们学习了如何手动创建、构建、部署和调用 Lambda 函数,并且我们必须经历一系列步骤才能使其正常工作。现在假设我们需要为一个银行应用程序向门户部署成百上千个这样的 Lambda 函数。要手动完成这项任务,我们需要大量的资源和时间。这时,DevOps 就变得非常有用,它可以使我们的工作更快、更简单。让我们仔细看看如何使用 DevOps 自动化所有相关步骤,使构建、测试和部署 Lambda 函数变得更加简便。

AWS Lambda 与 DevOps

要开始为 AWS Lambda 实现 DevOps,我们首先创建一个流水线。流水线概述了开发人员创建代码、测试代码并将代码提交到仓库时所涉及的各个阶段。源代码从仓库中提取出来,然后进行构建和测试。之后,进行静态代码分析。一旦部署到类似生产的环境中,就会对其进行验收测试。这就是应用程序如何被监控,以及如何管理日志。在本节中,我们将通过这些食谱来查看这些阶段。我们还将从两个角度来讨论 DevOps——一个是通过 AWS 自身的工具集,另一个是通过无服务器框架,如 GitHub、Jenkins、Mocha(用于测试)和 JSHint(用于源代码分析)。

所以第一步是设置一个本地开发环境,在这里我们可以创建文件夹结构,添加和更改源代码,添加图像等。我们可以在本地运行源代码,执行测试,并调试其中的错误和故障。我们将使用 Node.js 教程来完成这一过程。我们将从零开始设置一个 Node.js 项目,包含单元测试、源代码分析和验收测试。

我创建的 Node.js 应用是一个简单的任务管理器。其底层架构是 AWS Lambda 函数,配合 API 网关用于添加、更新、删除和列出 DynamoDB 表。简而言之,我们将通过 AWS Lambda 函数实现 createreadupdatedelete 功能。

与 AWS CodePipeline 的无服务器框架

正如我之前提到的,我们的第一种方法将通过 AWS 自己的 DevOps 工具来实现。我们将从 CodePipeline 开始,它是 AWS 原生的基于云的工具,可以帮助你在 AWS 上快速构建、开发和部署应用程序。我们可以通过 CodePipeline 快速设置持续交付。它有自己的仪表盘,且能轻松与 JIRA、Jenkins、GitHub 和其他项目管理工具集成。让我们来看看如何将其应用于 Lambda 函数。我们将使用本章前面创建的缩略图应用。

这些操作的先决条件如下:

  • AWS 账户和 AWS 控制台登录凭证: 本教程第一部分的大部分设置将通过 AWS 控制台完成。我们将使用之前在本章创建的相同 adminuser 账户。

  • GitHub 仓库: 你需要创建一个仓库,并将以下仓库中的所有文件和文件夹复制到你的仓库中:

    https://github.com/shzshi/aws-lambda-thumbnail.git

  • CloudFormation 服务角色: 进入 AWS 控制台首页并搜索 IAM。在 IAM 页面,选择 角色,然后点击创建角色。在创建角色页面,选择 AWS 服务,并选择 CloudFormation 作为服务。接着点击下一步:权限,在权限策略页面,选择 AWSLambdaExecute 策略,然后点击下一步:审核。一旦审核页面打开,给角色命名为 myCloudFormationRole,然后点击创建角色。现在,对于这个服务角色,我们需要添加额外的策略来执行管道,所以我们需要进入角色页面。我们将在列表中看到我们的角色,点击它。在角色摘要页面,点击添加内联策略,在创建策略页面,点击 JSON 标签,然后将现有的 JSON 脚本替换为 cloudformationpolicy.json 中的脚本,cloudformationpolicy.json 文件位于 aws-lambda-thumbnail 仓库中。点击审核策略。现在我们将策略命名为 myThumbnailPipelinePolicy,这样我们就为 CloudFormation 创建了一个服务角色。

  • CloudFormation 包的存储桶: 我们需要为 CloudFormation 包创建一个存储桶,所以我们需要进入 AWS 控制台首页并在服务中搜索 S3。接着,我们创建一个名为 my-cloud-formation-bucket 的存储桶。这个存储桶用于在我们运行管道时打包我们的工件。

让我们按照以下步骤进行操作:

  1. 要为 adminuser 添加并获取 CodeStar 权限,请访问 AWS 控制台(console.aws.amazon.com/console/home),并使用您的根账户凭证登录。这意味着我们需要作为一个免费账户用户登录(我们在章节开始时创建了这个免费账户)。如果你进入 adminuser 登录页面,你会看到页面底部有一个名为“使用根账户凭证登录”的链接。登录后,进入 IAM 服务并点击“用户”。你应该能够在列表中看到 adminuser。现在点击 adminuser 链接,进入“安全凭证”标签。向下滚动,找到“HTTPS Git 凭证用于 AWS CodeCommit”部分,然后点击“生成”按钮。此时将生成用于认证 AWS CodeCommit 的凭证。复制或下载这些凭证。如果你没有复制访问密钥 ID 和密钥访问密钥,请使用“创建访问密钥”按钮生成新的密钥。将这两个密钥的详细信息保存以供以后使用。

  2. 现在我们以 adminuser 身份登录控制台。从主页搜索 CodePipeline。页面将会打开。在这个页面中,点击“创建管道”。然后你将被重定向到“创建管道”页面。我们将管道命名为 myServerlessThumbnailPipeline 并点击“下一步”。

  3. 在源提供程序中,选择 GitHub。接下来我们将被要求连接到 GitHub,请使用你的凭证进行连接。仓库应为我们之前作为先决条件创建的仓库,分支应为我们的文件所在的分支(例如,master)。输入完细节后,点击“下一步”。在设置 CodeBuild 时,创建了一个角色,因此我们需要为此角色添加额外的策略。

  4. 在构建提供程序中,选择 AWS CodeBuild,然后在配置项目部分中,选择创建一个新的构建项目。现在我们来添加项目细节:项目名称应为myThumbnailCodeBuild,环境镜像应选择“使用 AWS CodeBuild 管理的镜像”,操作系统应选择 Ubuntu,运行时应选择 Node.js,版本应为 Node.js 6.3.1。其余的细节保持默认值,然后点击“保存构建项目”。我们已成功创建了一个 AWS CodeBuild 项目。不过,它也创建了一个服务角色,我们需要为 CodeBuild 项目角色添加一个额外的策略。所以现在我们打开浏览器的新标签页,并以 adminuser 身份登录 AWS 控制台。接着,在服务中搜索 IAM,在 IAM 页面中,进入角色并选择名为 code-build-myThumbnailCodeBuild-service-role 或类似名称的服务角色。

现在点击“Add inline policy”,然后点击“Create Policy”页面。选择 S3 服务和“PubObject”操作(在写入访问级别下),并选择资源为所有资源。最后,点击“Review Policy”。将策略命名为myThumbnailCodeBuildPolicy。在摘要部分,我们应该能够看到 S3。点击“Create Policy”。现在我们已经为 S3 添加了一个新策略,并将其添加到 CodeBuild 角色中。接下来,我们回到创建 CodePipeline 页面。点击“Next step”。

  1. 在“Deploy template”中,我们将“Deployment provider”设置为 AWS CloudFormation。现在我们进入 CloudFormation 部分,按以下截图添加所有详细信息。模板文件基本上是一个导出文件,将由 CloudFormation 使用:

** **

  1. 接下来,我们将创建一个角色,以授予 AWS CodePipeline 使用资源的权限。点击“Create role”并按照提示完成步骤。角色创建后,点击“Next”。然后审查管道并点击“Create pipeline”。我们的管道将自动触发。管道的第一个阶段正常工作。如果我们进入 CloudFormation(服务 | CloudFormation),应该能看到为缩略图创建的堆栈。如果勾选复选框并选择事件,我们应该能看到已运行的事件以及其他一些详细信息。接下来,我们将继续向管道添加更多可以批准的阶段,然后部署该函数。

  2. 现在,我们将编辑管道并添加一些阶段来部署缩略图 Lambda 函数。点击“编辑”,并向下滚动至底部。点击“+ 阶段”。我们添加一个名为Approval的阶段,以便在部署之前审查和批准我们的工作。点击“Action”,从下拉列表中的“Action”类别中选择“approval”。我们将其命名为Approval。我们还可以添加一个 SNS 主题来接收批准邮件。为此,我们继续使用默认值并点击“Add action”。

  3. 下一阶段是将函数从 Git 仓库部署到 Lambda 函数。点击“+ 阶段”。我们添加一个名为“Deploy”的阶段,点击“Action”,并从下拉列表中的“Action”类别中选择“Deploy”。接下来,我们将操作命名为“myDeploy”,并选择 AWS CloudFormation 作为部署提供商。在 CloudFormation 部分,我们添加一个名为“execute a change set”的操作模式,并选择堆栈名称“mythumbnailstack”和更改集名称“mythumbnailchangeset”。我们将其余细节保持为默认值并点击“Add action”。

  4. 现在我们已经添加了两个阶段,点击“Save Pipeline changes”保存管道。系统会提示我们继续操作,点击“Save and Continue”。这一次,管道不会自动触发,因此我们需要点击“Release change”来启动管道。管道成功完成后,我们应该能看到所有阶段都显示为绿色,如下图所示:

  1. 让我们检查一下函数是否已经创建,并尝试执行它。我们进入 AWS 控制台主页并搜索 Lambda。我们应该能看到已创建一个缩略图函数。点击打开 Lambda 函数。在函数页面上,向页面的右上角滚动,我们会看到一个下拉菜单,点击后可以添加事件。选择“配置测试事件”。弹出的框中会显示事件名称“myThumbnailEvent”以及一个文本字段。在文本字段中,添加以下 JSON 文件。确保将 my-source-bucket76 替换为你的源存储桶名称,将 Baby.jpg 替换为你的图像文件名。然后点击“保存”。

事件 JSON 文件可以在 gist.github.com/shzshi/7a498513ae43b6572c219843bbba277d 找到。

  1. 现在我们已经部署了函数,创建了 S3 存储桶和事件。让我们来调用这个函数。点击“测试”。现在你应该能看到函数成功执行的结果。你将看到日志中的详细信息,如果你刷新名为“resized”的 S3 存储桶,我们应该能看到一张已调整大小的图像文件。你可以直接下载这个调整大小后的文件,查看调整是否成功。我们还可以添加一个 S3 放置触发器,以便在任何图像文件上传到源 S3 存储桶时自动触发这个 CreateThumbnail 函数。

在本教程中,我们学习了如何使用 AWS 的 CD 平台 CodePipeline 来部署 Lambda 函数。使用 AWS 提供的各种工具组合,从 GitHub 部署函数到 Lambda 非常迅速。但这些工具的缺点是你需要为它们的使用付费,而且我们必须真正理解 CloudFormation 和角色的配置。现在让我们来看看如何使用开源工具设置一个流水线。

使用 Lambda 实现持续集成和持续部署

在这一部分,我们将使用 Jenkins、无服务器框架和其他开源软件来设置持续集成和持续部署。我已将整个项目设置在一个 Git 仓库中(github.com/shzshi/aws-lambda-dynamodb-mytasks.git),或者我们可以按照本部分列出的以下步骤进行操作。

本教程中使用的应用程序将创建一个 Lambda 函数和一个 AWS API 网关,以便我们测试 Lambda 函数,该函数将使用 CRUD 操作管理任务,并将数据存储在 DynamoDB 中。

首先,让我们使用无服务器框架创建一个文件夹结构,以创建一个模板函数,如下面的代码所示。我假设你使用的是 Linux 终端,并且所有的指令都是基于 Linux 终端的:

$ serverless create --template aws-nodejs --path AWSLambdaMyTask
$ cd AWSLambdaMyTask

我们将在AWSLambdaMyTask文件夹中看到创建的三个文件。这是一个使用无服务器框架的 Node.js 示例模板。我们将根据示例的需要修改这些文件,如下所示:

  1. 让我们在AWSLambdaMyTask文件夹中再创建两个文件夹,分别是srctestsrc文件夹是我们的源代码文件夹,test文件夹是我们的测试用例文件夹,如下所示:
$ mkdir test
$ mkdir src  
  1. 然后我们将使用编辑器创建一个名为package.json的文件。这个文件将保存与项目相关的元数据。请将以下内容复制到该文件中,并根据需要进行修改:
{
  "name": "AWS-serverless-with-dynamodb",
  "version": "1.0.0",
  "description": "Serverless CRUD service exposing a REST HTTP interface",
  "author": "Shashikant Bangera",
  "dependencies": {
    "uuid": "³.0.1"
  },
  "keywords": [
    "AWS",
    "Deployment",
    "CD/CI",
    "serverless",
    "task"
  ],
  "repository": {
    "type": "git",
    "url": ""
  },
  "bugs": {
    "url": ""
  },
  "author": "Shashikant Bangera <shzshi@gmail.com>",
  "license": "MIT",
  "devDependencies": {
    "AWS-sdk": "².6.7",
    "request": "².79.0",
    "mocha": "³.2.0",
    "serverless": "¹.7.0"
},
  "scripts": {
    "test": "./node_modules/.bin/mocha"
  }
}
  1. 根据我们的需求,让我们编辑serverless.yml文件,如下所示。你可以在下面提到的 GitHub 仓库中找到这个文件:github.com/shzshi/aws-lambda-dynamodb-mytasks/blob/master/serverless.yml
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!
service: AWSLambdaMyTask
frameworkVersion: ">=1.1.0 <2.0.0"
provider:
  name: AWS
  runtime: nodejs4.3
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:AWS:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
functions:
  create:
    handler: src/mytasks/create.create
---
  list:
    handler: src/mytasks/list.list
---
  get:
    handler: src/mytasks/get.get
---
  update:
    handler: src/mytasks/update.update
---
  delete:
    handler: src/mytasks/delete.delete
---
resources:
  Resources:
    mytasksDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
  1. 让我们移动src目录,并创建一个名为package.json的文件和一个名为mytasks的文件夹,如下所示。mytasks文件夹将包含 Node.js 文件,用于在 AWS 上创建、删除、获取、列出和更新 DynamoDB 表:
$ cd src
$ mkdir mytasks
$ vim package.json
  1. 将以下内容复制到package.json
{
  "name": "src",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "uuid": "².0.3"
  }
}

进入mytasks文件夹,创建一个create.js文件,用于创建、更新、列出、获取和删除 DynamoDB 表。create.js文件是 Lambda 函数的处理程序。

  1. 将以下内容添加到src\mytasks\create.js
'use strict';
const uuid = require('uuid');
const AWS = require('AWS-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.create = (event, context, callback) => {
  const timestamp = new Date().getTime();
  const data = JSON.parse(event.body);
  if (typeof data.text !== 'string') {
    console.error('Validation Failed');
    callback(null, {
      statusCode: 400,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Couldn\'t create the task item.',
    });
    return;
  }
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Item: {
      id: uuid.v1(),
      text: data.text,
      checked: false,
      createdAt: timestamp,
      updatedAt: timestamp,
    },
  };
  // write the task to the database
  dynamoDb.put(params, (error) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t create the task item.',
      });
      return;
    }
    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(params.Item),
    };
    callback(null, response);
  });
};
  1. 将以下内容添加到src\mytasks\delete.js
'use strict';
const AWS = require('AWS-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.delete = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };
  // delete the task from the database
  dynamoDb.delete(params, (error) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t remove the task item.',
      });
      return;
    }
    // create a response
    const response = {
statusCode: 200,
      body: JSON.stringify({}),
    };
    callback(null, response);
  });
};
  1. 将以下内容添加到src\mytasks\get.js
'use strict';
const AWS = require('AWS-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.get = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };
  // fetch task from the database
  dynamoDb.get(params, (error, result) => {
    // handle potential errors
if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t fetch the task item.',
      });
      return;
    }
    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Item),
    };
    callback(null, response);
  });
};
  1. 将以下内容添加到src\mytasks\list.js
'use strict';
const AWS = require('AWS-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
const params = {
  TableName: process.env.DYNAMODB_TABLE,
};
module.exports.list = (event, context, callback) => {
  // fetch all tasks from the database
  dynamoDb.scan(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t fetch the tasks.',
      });
      return;
    }
    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Items),
    };
    callback(null, response);
  });
};
  1. 将以下内容添加到src\mytasks\update.js
'use strict';
const AWS = require('AWS-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.update = (event, context, callback) => {
  const timestamp = new Date().getTime();
  const data = JSON.parse(event.body);
  // validation
  if (typeof data.text !== 'string' || typeof data.checked !== 'boolean') {
    console.error('Validation Failed');
    callback(null, {
      statusCode: 400,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Couldn\'t update the task item.',
    });
    return;
  }
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
id: event.pathParameters.id,
    },
    ExpressionAttributeNames: {
      '#task_text': 'text',
    },
    ExpressionAttributeValues: {
      ':text': data.text,
      ':checked': data.checked,
      ':updatedAt': timestamp,    },
    UpdateExpression: 'SET #task_text = :text, checked = :checked,
updatedAt = :updatedAt',
    ReturnValues: 'ALL_NEW',
  };
  // update the task in the database
  dynamoDb.update(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t fetch the task item.',
      });
      return;
    }
// create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Attributes),
    };
    callback(null, response);
  });
};

现在我们将创建测试用例来单元测试我们创建的代码。我们将使用 Mocha 进行单元测试,并重新运行 API。让我们在test文件夹中创建一个名为data的文件夹,如下所示。该文件夹将包含单元测试将要使用的 JSON 数据:

$ mkdir test/data
  1. 接下来,让我们添加test/createDelete.js文件,该文件将在测试完成后创建 DynamoDB 数据并删除它,如下所示:
var assert = require('assert');
var request = require('request');
var fs = require('fs');
describe('Create, Delete', function() {
            this.timeout(5000);
    it('should create a new Task, & delete it', function(done) {
                        // Build and log the path
                        var path = "https://" + process.env.TASKS_ENDPOINT + "/mytasks";
                        // Fetch the comparison payload
                        require.extensions['.txt'] = function (module, filename) {
                            module.exports = fs.readFileSync(filename, 'utf8');
                        };
                        var desiredPayload = require("./data/newTask1.json");
                        // Create the new Task
                        var options = {'url' : path, 'form': JSON.stringify(desiredPayload)};
                        request.post(options, function (err, res, body){
                                    if(err){
                                                throw new Error("Create call failed: " + err);
                                    }
                                    assert.equal(200, res.statusCode, "Create Status Code != 200 (" + res.statusCode + ")");
                                    var task = JSON.parse(res.body);
                                    // Now delete the task
                                    var deletePath = path + "/" +
task.id;
                                   request.del(deletePath, function (err, res, body){
                                                if(err){
                                                            throw new Error("Delete call failed: " + err);
                                                }
                                                assert.equal(200, res.statusCode, "Delete Status Code != 200 (" + res.statusCode + ")");
                                                done();
                                    });
                        });
    });
});
  1. 现在添加test/createListDelete.js文件,该文件将在测试完成后创建 DynamoDB 数据、列出它并删除它,如下所示:
var assert = require('assert');
var request = require('request');
var fs = require('fs');
describe('Create, List, Delete', function() {
            this.timeout(5000);
    it('should create a new task, list it, & delete it', function(done) {
                        // Build and log the path
----
                        // Fetch the comparison payload
                        require.extensions['.txt'] = function (module, filename) {
----
                        // Create the new Task
                        var options = {'url' : path, 'form': JSON.stringify(desiredPayload)};
                        request.post(options, function (err, res, body){
                                    if(err){
                                                throw new Error("Create call failed: " + err);
                                    }
                                    assert.equal(200, res.statusCode, "Create Status Code != 200 (" + res.statusCode + ")");
// Read the list, see if the new item is there at the end
                                    request.get(path, function (err,
res, body){
                                                if(err){
                                                            throw new Error("List call failed: " + err);
                                                }
                                                assert.equal(200, res.statusCode, "List Status Code != 200 (" + res.statusCode + ")");
                                                var taskList = JSON.parse(res.body);
                                                if(taskList[taskList.length-1].text = desiredPayload.text)     {
                                                            // Item found, delete it
-----
                                                                        assert.equal(200, res.statusCode, "Delete Status Code != 200 (" + res.statusCode + ")");
                                                                        done();
                                                            });
                                                } else {
                                                            // Item not found, fail test
                                                            assert.equal(true, false, "New item not found in list.");
                                                            done();
                                                }
});
                        });
    });
});
  1. 让我们添加test/createReadDelete.js文件,该文件将在测试完成后创建 DynamoDB 数据、读取它并删除它,如下所示:
var assert = require('assert');
var request = require('request');
var fs = require('fs');
describe('Create, Read, Delete', function() {
            this.timeout(5000);
    it('should create a new Todo, read it, & delete it', function(done) {
                        // Build and log the path
                        var path = "https://" + process.env.TASKS_ENDPOINT + "/mytasks";
                        // Fetch the comparison payload
                        require.extensions['.txt'] = function (module, filename) {
                            module.exports = fs.readFileSync(filename, 'utf8');
                        };
                        var desiredPayload = require("./data/newTask1.json");
                        // Create the new todo
                        var options = {'url' : path, 'form': JSON.stringify(desiredPayload)};
                        request.post(options, function (err, res, body){
if(err){
                                                throw new Error("Create call failed: " + err);
                                    }
                                    assert.equal(200, res.statusCode, "Create Status Code != 200 (" + res.statusCode + ")");
                                    var todo = JSON.parse(res.body);
                                    // Read the item
                                    var specificPath = path + "/" + todo.id;
                                    request.get(path, function (err, res, body){
                                                if(err){
                                                            throw new Error("Read call failed: " + err);
                                                }
                                                assert.equal(200,
res.statusCode, "Read Status Code != 200 (" + res.statusCode + ")");
                                                var todoList = JSON.parse(res.body);
                                                if(todoList.text = desiredPayload.text)          

                                                            // Item found, delete it
                                                            request.del(specificPath, function (err, res, body){
                                                                        if(err){
                                                                                    throw new Error("Delete call failed: " + err);
                                                                        }
                                                                        assert.equal(200, res.statusCode, "Delete Status Code != 200 (" + res.statusCode + ")");
                                                                        done();
                                                            });
} else {
                                                            // Item not found, fail test
                                                            assert.equal(true, false, "New item not found in list.");
                                                            done();
                                                }
                                    });
                        });
    });
});

现在我们将创建两个测试数据文件——newTask1.jsonnewTask2.json——可以用于单元测试。

  1. 让我们使用上述数据创建data/newTask1.json,如下所示:
{ "text": "Learn Serverless" }
  1. 将以下 JSON 数据添加到data/newTask2
{ "text": "Test Serverless" }

项目文件夹现在应该像以下截图所示:

我们需要在 Git 上创建一个仓库,将之前创建的所有代码推送到仓库,以便我们可以为无服务器项目设置 CI。我假设 Git 已经安装在本地服务器上,且 Git 仓库已经建立。在我的例子中,我已经设置了以下 Git 仓库。我将执行git clone,添加文件和文件夹,然后将所有内容推送到 Git 仓库:

$ git clone https://github.com/shzshi/AWS-lambda-dynamodb-mytasks.git

将会有一个名为AWS-lambda-dynamodb-mytasks的文件夹。进入该目录,复制我们之前创建的所有文件,然后将它们推送到仓库,如以下代码所示:

$ git add .
$ git commit –m “my first commit”
$ git push origin master

为无服务器应用程序设置 Jenkins

假设我们已经启动并运行了 Jenkins,我们需要安装 Node.js,然后在 Jenkins 服务器上安装 Mocha 进行单元测试。之后,我们需要安装 Serverless Framework。你可以使用上面提到的 GitHub 仓库中的 Dockerfile(github.com/shzshi/aws-lambda-dynamodb-mytasks/blob/master/Dockerfile)为 Jenkins 和 Serverless 框架配置。如果你使用 Docker,则无需按照 Jenkins 上安装 Node.js 的步骤操作。

按照以下步骤在 Jenkins 节点上安装 Node.js:

$ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ sudo npm install -g serverless

然后打开浏览器并访问 Jenkins 主页。点击“New item”链接。这将打开一个新页面,允许你创建一个自定义名称的作业。选择“Freestyle project”,这是默认选择,然后点击“OK”继续,如下截图所示:

现在,我们需要将 Git 源代码与 Jenkins 集成,然后构建、部署并测试我们的无服务器应用程序。首先,让我们将 Git 仓库添加到 Jenkins 作业中,如下截图所示:

我们需要将构建参数化,添加AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY,如下截图所示。在我们为 Serverless Framework 创建 IAM 用户后,将获得AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY

接下来,转到“Build | Add build step | Execute shell | Execute build step | Add build step”从下拉菜单中选择,这将打开命令提示符,我们将在其中添加需要运行的命令,如下截图所示:

一旦构建成功,我们就成功将应用程序部署到 Serverless Framework 创建的 AWS S3 桶中。我们还将暴露 API 和函数,允许它们被应用程序使用,以执行 CRUD 操作,如下截图所示:

Lambda 函数的自动化测试

在之前的配方中,我们查看了如何通过 Jenkins 和 Serverless Framework 自动化构建和部署 Lambda 函数。但我们还可以通过 Jenkins 对已部署的 Lambda 函数进行单元测试。在接下来的配方中,我们将看到如何对 Lambda 函数进行单元测试,检查该函数是否完美部署并正常工作。

部署应用程序的单元测试

一旦应用程序部署完成,我们可以在其上运行单元测试。为了进行单元测试,我在test文件夹中创建了三个单元测试,我们将使用 Mocha 来测试它们。接下来,在 Jenkins 中创建另一个任务来设置单元测试。这个任务同样是 Jenkins 中的一个自由风格任务,如下图所示:

我们将测试用例所在的 Git 仓库路径添加到 Jenkins 中,如下图所示:

然后,我们添加一个执行 Shell 命令来使用 Mocha 运行单元测试,如下图所示:

如下图所示,我们的测试用例通过了,这证明我们的函数运行得非常正常:

AWS Lambda 流水线

在本章的这一部分,我们将转向持续交付流水线,继续我们之前做的事情:创建不同的环境,部署到多个环境,进行单元测试,系统测试,并添加审批流程。在这个流水线中,我们将使用 Jenkins、Groovy、Serverless Framework 和 Docker 来设置 Serverless Framework 环境。请确保你已在机器上安装了 Docker。

前提条件

我们需要为每个阶段或环境创建一个用户,以便隔离环境,并为每个环境设定部署的阶段。为此,请为每个用户执行以下步骤:

  1. 以 root 用户登录 AWS 账户,并进入 IAM(身份和访问管理)页面。

  2. 在左侧栏点击“用户”,然后点击“添加用户”按钮,添加用户名 dev-serverless。通过勾选复选框启用程序化访问。然后点击“下一步:权限”按钮。

  3. 在权限页面,选择“直接附加现有策略”,搜索并选择“AdministratorAccess”复选框。然后点击“下一步:审核”。

  4. 现在检查一切是否正常,然后点击“创建用户”。这将创建一个用户,并展示给我们“访问密钥 ID”和“秘密访问密钥”。将这些密钥临时复制到其他地方。

  5. 现在我们已经获取了密钥,将它们作为环境变量导出,这样框架就能访问这些变量,执行所需的功能。

  6. 对 sit-serverless 和 prod-serverless 用户重复前面五个步骤。

  7. CloudBees AWS Credentials Jenkins 插件。

现在,按照以下步骤创建流水线:

  1. 使用 Git 克隆以下仓库到一个目录:
$ git clone https://github.com/shzshi/aws-lambda-dynamodb-mytasks.git
  1. 进入此目录并使用提供的 Dockerfile 构建 Docker 镜像。通过 docker images,我们应该能看到名为 docker build --rm -f Dockerfile -t aws-lambda-dynamodb-mytasks:latest 的 Docker 镜像,如以下代码所示:
$ cd aws-lambda-dynamodb-mytasks
$ docker build --rm -f Dockerfile -t aws-lambda-dynamodb-mytasks:latest .
$ docker images
  1. 接下来,我们将运行容器并在浏览器中打开 Jenkins。初始安装密码可以在容器运行日志中找到,如以下代码所示:
$ mkdir jenkins
$ docker run --rm -it -p 50000:50000 -p 8080:8080 -v <FULL_PATH_TO_JENKINS_FOLDER>/jenkins:/var/jenkins_home  aws-lambda-dynamodb-mytasks:latest
  1. 打开浏览器并访问 http://localhost:8080。从容器运行中复制密码,它应该类似于以下输出。一旦你登录,安装推荐的插件并为将来登录创建一个 Jenkins 用户。

Jenkins 初始设置将是必需的。一个管理员用户已经被创建,并且密码已生成。使用以下密码继续安装:

6050bfe89a9b463c8e2784060e2225b6

这也可以在 /var/jenkins_home/secrets/initialAdminPassword 中找到。

一旦 Jenkins 启动并运行,我们可以继续创建一个流水线作业,所以点击 New Item,输入项目名称为 my-serverless-pipeline,并选择流水线项目。在 Job Configure 中,选择 This project is parameterized 复选框,然后在 Add Parameter 中选择 Credentials Parameter,接着进入 Credentials Parameter 的 Default Value 部分并点击 Add。然后选择 Jenkins。这将打开 Jenkins Credentials Provider 页面。在该页面的 Kind 下拉菜单中,选择 AWS Credentials 并添加用户 dev-serverlesssit-serverlessprod-serverless,如以下截图所示。然后点击 Add:

一旦所有 AWS 凭证被添加,拉取它们到 AWS 的 Credentials 参数中,如以下截图所示。确保所有三种凭证参数都已添加,即 devsitprod

你需要创建自己的 Git 仓库,并推送文件到名为 github.com/shzshi/aws-lambda-dynamodb-mytasks.git 的仓库中。然后,在你喜欢的编辑器中打开 Jenkins 文件,并取消注释整个环境的所有系统测试代码,如以下代码所示。我们这么做的原因是我们将导出 API 网关端点来执行整个环境的系统测试:

stage ('System Test on Dev') {
steps {
    withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'dev-serverless', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) {
    sh ''' 
        export TASKS_ENDPOINT=6pgn5wuqeh.execute-api.us-east-1.amazonaws.com/dev
./node_modules/mocha/bin/mocha ./test/*.js
'''
   }
  }
}

现在点击 Pipeline 标签,并选择 Pipeline script 作为定义中的 SCM。将 SCM 设置为 Git,并在 Repository URL 字段中添加你创建的 Git 仓库路径。这个仓库中包含了 Jenkins 文件、Lambda 函数和测试文件夹。其余部分保持默认设置并点击 Save。现在我们的流水线已经保存。接下来是运行流水线。Jenkinsfile 是一个出色的脚本,它将为我们协调流水线的执行。

点击“使用参数构建”。然后你会看到我们的管道构建、测试和部署代码所需要的基于环境的构建参数。

第一次运行应该没有任何测试,仅仅是部署函数和 API 网关到 AWS 云中的任务。控制台输出将提供任务的端点,如下代码所示:

endpoints:
  POST - https://6pgn5wuqeh.execute-api.us-east-1.amazonaws.com/dev/mytasks
  GET - https://6pgn5wuqeh.execute-api.us-east-1.amazonaws.com/dev/mytasks
  GET - https://6pgn5wuqeh.execute-api.us-east-1.amazonaws.com/dev/mytasks/{id}
  PUT - https://6pgn5wuqeh.execute-api.us-east-1.amazonaws.com/dev/mytasks/{id}
  DELETE - https://6pgn5wuqeh.execute-api.us-east-1.amazonaws.com/dev/mytasks/{id}

用控制台中列出的 API 网关路径替换 Jenkinsfile 中整个环境的任务端点,如下代码所示。然后保存 Jenkinsfile 并将其推送到你创建的 Git 仓库中。我们之所以在构建后期添加端点,是因为 API 网关端点是动态创建的。但我们也可以拥有一个带有自定义域名的静态端点 URL,API 网关中提供了这一功能:

export TASKS_ENDPOINT=6pgn5wuqeh.execute-api.us-east-1.amazonaws.com/dev 

点击“使用参数构建”来构建作业。在这里,我们应该能够看到系统测试正在与部署步骤一起运行,管道应显示为绿色,如下截图所示:

在前面的示例中,我们学习了 Lambda 函数如何与 API 网关进行任务调用。我们还了解了它们如何通过 Serverless 框架和 Jenkins 部署到 AWS,涉及不同的环境,如开发(dev)、系统集成(sit)和生产(prod)。我们还创建并测试了系统测试。部署和执行将把 Lambda 函数和 API 网关部署到 AWS 云中,并且每个系统测试将执行 Lambda 函数对 DynamoDB 进行 CRUD 操作。因此,如果你进入 DynamoDB,你应该看到为每个环境创建的三个表。你还应该能够看到每个环境的不同功能和 API 网关。

部署方法

部署到生产环境总是伴随着一定的困难。在部署到生产环境时,我们会采取很多预防措施。我们在低环境中进行大量的测试,以便尽早解决大部分的漏洞和性能问题。但是我们在部署到生产环境时仍然感到紧张,因为我们永远无法百分之百确定部署的版本是否能完美运行。如果我们使用了部署技术,那么就能大大减少部署失败的风险。部署技术有很多种,但金丝雀部署和蓝绿部署是最受欢迎的两种。我们将一起看一下这两种部署技术在 AWS Lambda 中的应用实例。

金丝雀部署

金丝雀部署是一种部署技术,涉及将生产流量逐步从版本 A 转移到版本 B,其中版本 B 是最新版本,版本 A 是之前的版本。AWS 最近引入了 Lambda 函数别名的流量转移。别名是指向特定版本 Lambda 函数的指针,这基本上意味着我们可以通过指定要转移到新版本的流量百分比,将函数的流量在两个不同版本之间进行拆分。当调用别名时,Lambda 会自动在版本之间负载均衡请求。因此,取代用另一个版本替换一个函数,两者可以共存,并且可以监控它们的表现。

这一切听起来很棒,但要做到这一点并不容易。幸运的是,AWS 已经有一个可以帮助我们解决这个问题的服务——CodeDeploy。要在 AWS CodeDeploy 服务中使用金丝雀部署,我们需要创建各种资源。我们需要创建一个 CodeDeploy 应用程序、一个部署组以及函数的别名。我们还需要创建新的权限,并将所有事件源替换为触发别名,而不是最新的函数。但如果我们使用与 Serverless Framework 配合使用的金丝雀部署插件,这一切会变得更简单。让我们通过一个例子学习如何实现这一点。

我们将在接下来的食谱中使用的代码可以在以下网址找到:github.com/shzshi/my-canary-deployment.git

设置一个简单的环境

执行以下步骤:

  1. 让我们使用以下命令创建一个简单的 serverless 服务。将创建两个文件,分别为 handler.jsserverless.yml
$ serverless create --template aws-nodejs --path my-canary-deployment
  1. 现在用以下代码替换 serverless.yml 的内容。确保它正确缩进。我们正在创建一个包含函数和 API 网关的服务:
service: my-canary-deployment 
provider:  
    name: aws  
    runtime: nodejs6.10 

plugins:  
    - serverless-plugin-canary-deployments 
functions:  
    hello:  
        handler: handler.hello  
        events:  
            - http: get hello
  1. 让我们用以下内容替换 handler.js 的内容:
module.exports.hello = (event, context, callback) => { 
    const response = { 
        statusCode: 200, 
        body: 'Go Serverless v1.0! Your function executed successfully!' 
    };

    callback(null, response);
};
  1. 创建一个 package.json 文件,如以下代码所示:
{
    "name": "my-canary-deployment",
    "version": "1.0.0",
    "description": "",
    "main": "handler.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "shashikant bangera",
    "devDependencies": {
        "serverless-plugin-aws-alerts": "¹.2.4",
        "serverless-plugin-canary-deployments": "⁰.4.0"
    }
}
  1. 让我们将此功能部署到 AWS,如以下代码所示。确保在部署之前已经配置好你的 AWS 访问密钥和秘密密钥:
$ npm install 
$ serverless deploy -v
  1. 部署成功后,让我们调用该函数并验证执行情况。我们将获得服务端点,所以让我们使用端点调用该函数,如以下代码所示:
$ curl https://<my-service-endpoint>-east-1.amazonaws.com/dev/hello

Go Serverless v1.0! Your function executed successfully!

设置金丝雀部署

一旦初始设置完成,我们将告诉 serverless 金丝雀部署插件将流量在最后两个版本之间分配,并逐渐将更多流量转移到新版本,直到它接收所有负载。

以下是我们可以使用 AWS 的 CodeDeploy 实现的三种渐进式部署类型:

  • 金丝雀: 流量在一段时间内转移到新版本,时间一到,所有流量将转移到新版本

  • 线性: 流量会按间隔逐渐转移到新版本,直到新版本接收到所有流量。

  • 一次性全部转移: 所有流量会一次性转移到新版本。

我们需要明确指定我们将使用的参数和部署类型,可以选择上述任何一种 CodeDeploy 部署选项:

Canary10Percent30MinutesCanary10Percent5MinutesCanary10Percent10MinutesCanary10Percent15MinutesLinear10PercentEvery10MinutesLinear10PercentEvery1MinuteLinear10PercentEvery2MinutesLinear10PercentEvery3MinutesAllAtOnce

对于我们的教程,我们将使用 Linear10PercentEvery1Minute,这意味着新版本的函数将每分钟增加 10% 的流量,直到达到 100%。为了实现这一点,我们需要在函数中设置类型和别名(我们想要创建的别名名称)下的 deploymentSettings。让我们更新文件,并重新部署并调用函数,看看流量是如何变化的:

  1. serverless.yml 中添加部署设置,如下方代码所示:
service: my-canary-deployment
provider:
 name: aws
 runtime: nodejs6.10

plugins:
 - serverless-plugin-canary-deployments
functions:
 hello:
     handler: handler.hello
     events:
         - http: get hello
     deploymentSettings:
 type: Linear10PercentEvery1Minute
 alias: Live
  1. 使用以下代码更新 handler.js
module.exports.hello = (event, context, callback) => {
    const response = {
    statusCode: 200,
    body: 'Hey new version is 1.26.1 !'
    };
    callback(null, response);
};
  1. 让我们部署函数。您将看到以下代码触发 CodeDeploy 进行线性部署,并且请求将在两个函数之间进行负载均衡:
$ serverless deploy -v

Serverless: Packaging service...
---
Serverless: Checking Stack update progress...
CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - my-canary-deployment-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Role - CodeDeployServiceRole
CloudFormation - CREATE_IN_PROGRESS - AWS::CodeDeploy::Application - MycanarydeploymentdevDeploymentApplication
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Function - HelloLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::CodeDeploy::Application - MycanarydeploymentdevDeploymentApplication
CloudFormation - CREATE_IN_PROGRESS - AWS::IAM::Role - CodeDeployServiceRole
CloudFormation - CREATE_COMPLETE - AWS::CodeDeploy::Application - MycanarydeploymentdevDeploymentApplication
CloudFormation - UPDATE_COMPLETE - AWS::Lambda::Function - HelloLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - HelloLambdaVersionW59z2S8rIu6lAv3dCyvgKLndpEosDs1l1kpbg6Lrg
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - HelloLambdaVersionW59z2S8rIu6lAv3dCyvgKLndpEosDs1l1kpbg6Lrg
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Version - HelloLambdaVersionW59z2S8rIu6lAv3dCyvgKLndpEosDs1l1kpbg6Lrg
CloudFormation - CREATE_COMPLETE - AWS::IAM::Role - CodeDeployServiceRole
CloudFormation - CREATE_IN_PROGRESS - AWS::CodeDeploy::DeploymentGroup - HelloLambdaFunctionDeploymentGroup
CloudFormation - CREATE_IN_PROGRESS - AWS::CodeDeploy::DeploymentGroup - HelloLambdaFunctionDeploymentGroup
CloudFormation - CREATE_COMPLETE - AWS::CodeDeploy::DeploymentGroup - HelloLambdaFunctionDeploymentGroup
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Alias - HelloLambdaFunctionAliasLive
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Alias - HelloLambdaFunctionAliasLive
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Alias - HelloLambdaFunctionAliasLive
CloudFormation - UPDATE_IN_PROGRESS - AWS::ApiGateway::Method - ApiGatewayMethodHelloGet
CloudFormation - UPDATE_COMPLETE - AWS::ApiGateway::Method - ApiGatewayMethodHelloGet
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Permission - HelloLambdaPermissionApiGateway
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Permission - HelloLambdaPermissionApiGateway
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530401347906
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530401347906
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530401347906
CloudFormation - UPDATE_COMPLETE - AWS::Lambda::Permission - HelloLambdaPermissionApiGateway
CloudFormation - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS - AWS::CloudFormation::Stack - my-canary-deployment-dev
CloudFormation - DELETE_IN_PROGRESS - AWS::Lambda::Permission - HelloLambdaPermissionApiGateway
CloudFormation - DELETE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530398711004
CloudFormation - DELETE_SKIPPED - AWS::Lambda::Version - HelloLambdaVersionjAYLrhwIiK3mGoae1oyrqMYfnXsHGIk0IuE6gh2dWdA
CloudFormation - DELETE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530398711004
CloudFormation - DELETE_COMPLETE - AWS::Lambda::Permission - HelloLambdaPermissionApiGateway
CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - my-canary-deployment-dev
  1. 让我们调用函数,看看它是否已经实现负载均衡,如下方代码所示:
$ curl https://<api-gateway-path>.amazonaws.com/dev/hello
Go Serverless v1.0! Your function executed successfully!

$ curl https://<api-gateway-path>.amazonaws.com/dev/hello
Hey new version is 1.26.1!

确保部署正常工作

我们的函数部署没有任何问题,但我们如何确保整个系统的正确运行呢?为了确保一切正常工作,我们可以向 CodeDeploy 提供一个变量列表,在部署过程中进行追踪,然后取消部署,如果触发了 ALARM,则将所有流量切换到旧版本。在无服务器架构中,我们可以使用另一个插件来设置警报。让我们看看如何操作:

  1. 更新 serverless.yml 来设置警报,如下方代码所示:
service: my-canary-deployment
provider:
 name: aws
 runtime: nodejs6.10

plugins:
    - serverless-plugin-aws-alerts
    - serverless-plugin-canary-deployments

custom:
 alerts:
 dashboards: true

functions:
    hello:
         handler: handler.hello
         events:
             - http: get hello
         alarms:
 - name: test
 namespace: 'AWS/Lambda'
 metric: Errors
 threshold: 1
 statistic: Minimum
 period: 60
 evaluationPeriods: 1
 comparisonOperator: GreaterThanOrEqualToThreshold
         deploymentSettings:
             type: Linear10PercentEvery1Minute
             alias: Live
             alarms:
 - HelloTestAlarm
  1. 让我们部署函数,看看它是如何工作的,如下方代码所示:
$ serverless deploy -v

Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (611 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - my-canary-deployment-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudWatch::Dashboard - AlertsDashboard
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Function - HelloLambdaFunction
CloudFormation - UPDATE_COMPLETE - AWS::Lambda::Function - HelloLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudWatch::Alarm - HelloTestAlarm
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudWatch::Alarm - HelloTestAlarm
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - HelloLambdaVersionja19jdYXntxmsiUagZLZfDEMTshQJ8ApOagyYwmXE
CloudFormation - CREATE_COMPLETE - AWS::CloudWatch::Alarm - HelloTestAlarm
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - HelloLambdaVersionja19jdYXntxmsiUagZLZfDEMTshQJ8ApOagyYwmXE
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Version - HelloLambdaVersionja19jdYXntxmsiUagZLZfDEMTshQJ8ApOagyYwmXE
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudWatch::Dashboard - AlertsDashboard
CloudFormation - CREATE_COMPLETE - AWS::CloudWatch::Dashboard - AlertsDashboard
CloudFormation - UPDATE_IN_PROGRESS - AWS::CodeDeploy::DeploymentGroup - HelloLambdaFunctionDeploymentGroup
CloudFormation - UPDATE_COMPLETE - AWS::CodeDeploy::DeploymentGroup - HelloLambdaFunctionDeploymentGroup
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Alias - HelloLambdaFunctionAliasLive
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Alias - HelloLambdaFunctionAliasLive
CloudFormation - UPDATE_COMPLETE - AWS::Lambda::Alias - HelloLambdaFunctionAliasLive
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530403890255
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530403890255
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530403890255
CloudFormation - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS - AWS::CloudFormation::Stack - my-canary-deployment-dev
CloudFormation - DELETE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530401797330
CloudFormation - DELETE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeployment1530401797330
CloudFormation - DELETE_SKIPPED - AWS::Lambda::Version - HelloLambdaVersionW59z2S8rIu6lAv3dCyvgKLndpEosDs1l1kpbg6Lrg
CloudFormation - UPDATE_COMPLETE - AWS::CloudFormation::Stack - my-canary-deployment-dev

部署步骤将在 CodeDeploy 和 CloudWatch 仪表板中创建一个警报,在那里我们可以看到不同的图表,表示调用和错误。您可以登录 AWS 控制台,进入 CodeDeploy 和 CloudWatch,查看警报是如何创建的,并查看仪表板的样子。

部署 CodeDeploy 钩子

现在我们拥有了所有工具来尽量减少可能的错误或故障的影响。然而,我们也可以通过首先运行 CodeDeploy 钩子来避免调用包含错误的函数版本。钩子是由 CodeDeploy 在流量切换之前和之后触发的 Lambda 函数。它期望收到钩子成功或失败的通知,只有在成功的情况下才会继续进行下一步。这些钩子非常适合运行集成测试并检查一切是否在云端配合得当,因为一旦失败,系统会自动回滚。

让我们通过以下步骤来看看如何创建钩子:

  1. 让我们更新serverless.yml以添加钩子详细信息。我们需要授予我们的函数访问 CodeDeploy 的权限,这样我们就可以在钩子中使用 CodeDeploy 的 SDK,如以下代码所示:
service: my-canary-deployment 
provider: 
    name: aws
    runtime: nodejs6.10
    iamRoleStatements:
    - Effect: Allow
    Action:
        - codedeploy:*
    Resource:
        - "*" 
plugins: 
    - serverless-plugin-aws-alerts
    - serverless-plugin-canary-deployments

custom:
    alerts:
        dashboards: true

functions:
    hello:
        handler: handler.hello
        events:
            - http: get hello
        alarms:
            - name: test
            namespace: 'AWS/Lambda'
            metric: Errors
            threshold: 1
            statistic: Minimum
            period: 60
            evaluationPeriods: 1
            comparisonOperator: GreaterThanOrEqualToThreshold
        deploymentSettings:
            type: Linear10PercentEvery1Minute
            alias: Live
            preTrafficHook: preHook
            postTrafficHook: postHook
            alarms:
                - HelloTestAlarm
    preHook:
        handler: hooks.pre 
 postHook:
        handler: hooks.post
  1. 接下来,我们将创建钩子。让我们在服务目录中创建一个名为hooks.js的新文件,并添加以下钩子内容:
const aws = require('aws-sdk');
const codedeploy = new aws.CodeDeploy({apiVersion: '2014-10-06'});
module.exports.pre = (event, context, callback) => {
 var deploymentId = event.DeploymentId;
 var lifecycleEventHookExecutionId = event.LifecycleEventHookExecutionId;

console.log('We are running some integration tests before we start shifting traffic...');

var params = {
 deploymentId: deploymentId,
 lifecycleEventHookExecutionId: lifecycleEventHookExecutionId,
 status: 'Succeeded' // status can be 'Succeeded' or 'Failed'
 };

return codedeploy.putLifecycleEventHookExecutionStatus(params).promise()
 .then(data => callback(null, 'Validation test succeeded'))
 .catch(err => callback('Validation test failed'));
};

module.exports.post = (event, context, callback) => {
 var deploymentId = event.DeploymentId;
 var lifecycleEventHookExecutionId = event.LifecycleEventHookExecutionId;
 console.log('Check some stuff after traffic has been shifted...');
 var params = {
 deploymentId: deploymentId,
 lifecycleEventHookExecutionId: lifecycleEventHookExecutionId,
 status: 'Succeeded' // status can be 'Succeeded' or 'Failed'
 };
 return codedeploy.putLifecycleEventHookExecutionStatus(params).promise()
 .then(data => callback(null, 'Validation test succeeded'))
 .catch(err => callback('Validation test failed'));
};
  1. 现在我们已经创建了钩子,让我们通过以下代码进行部署并查看它们的功能:
$ serverless deploy -v
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (1.12 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudFormation::Stack - my-canary-deployment-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::Logs::LogGroup - PreHookLogGroup
CloudFormation - CREATE_IN_PROGRESS - AWS::Logs::LogGroup - PostHookLogGroup
CloudFormation - CREATE_IN_PROGRESS - AWS::Logs::LogGroup - PreHookLogGroup
CloudFormation - UPDATE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::Logs::LogGroup - PostHookLogGroup
CloudFormation - UPDATE_IN_PROGRESS - AWS::CloudWatch::Dashboard - AlertsDashboard
CloudFormation - CREATE_COMPLETE - AWS::Logs::LogGroup - PreHookLogGroup
CloudFormation - CREATE_COMPLETE - AWS::Logs::LogGroup - PostHookLogGroup
CloudFormation - UPDATE_COMPLETE - AWS::CloudWatch::Dashboard - AlertsDashboard
CloudFormation - UPDATE_COMPLETE - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - PostHookLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - PreHookLambdaFunction
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Function - HelloLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - PreHookLambdaFunction
CloudFormation - UPDATE_COMPLETE - AWS::Lambda::Function - HelloLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Function - PostHookLambdaFunction
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Function - PostHookLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - HelloLambdaVersionUutX83EhRCt0XFaMjWRyD8vAkoceeNRnZXqaeuCkFo
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Function - PreHookLambdaFunction
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - PostHookLambdaVersionI0mvapPGCoXwaOw5oaAUMCSPtZYGXKy9YJABLTU
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - HelloLambdaVersionUutX83EhRCt0XFaMjWRyD8vAkoceeNRnZXqaeuCkFo
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Version - HelloLambdaVersionUutX83EhRCt0XFaMjWRyD8vAkoceeNRnZXqaeuCkFo
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - PostHookLambdaVersionI0mvapPGCoXwaOw5oaAUMCSPtZYGXKy9YJABLTU
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Version - PostHookLambdaVersionI0mvapPGCoXwaOw5oaAUMCSPtZYGXKy9YJABLTU
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - PreHookLambdaVersionG9WhVjc3o3mP7moxWMFYoj0jCN4eO1jJBUgGLJmcJA
CloudFormation - CREATE_IN_PROGRESS - AWS::Lambda::Version - PreHookLambdaVersionG9WhVjc3o3mP7moxWMFYoj0jCN4eO1jJBUgGLJmcJA
CloudFormation - CREATE_COMPLETE - AWS::Lambda::Version - PreHookLambdaVersionG9WhVjc3o3mP7moxWMFYoj0jCN4eO1jJBUgGLJmcJA
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Alias - HelloLambdaFunctionAliasLive
CloudFormation - UPDATE_IN_PROGRESS - AWS::Lambda::Alias - HelloLambdaFunctionAliasLive

当 CLI 上的部署仍在继续时,我们应该登录到 AWS Cloud 控制台并转到“服务”|“CloudFormation”。选择名为 my-canary-deployment-dev 的堆栈,向下滚动并选择状态原因列中的 CodeDeploy 链接。我们应该能够看到流量逐步转移的过程,包括预钩子、流量转移和后钩子执行,最终完成整个堆栈的部署。

在这里,我们学习了如何使用 Serverless 框架、各种插件和 AWS CodeDeploy 来设置金丝雀部署。

蓝绿部署

像金丝雀部署一样,蓝绿部署是另一种用于生产环境安全部署的方法。在金丝雀部署中,我们逐渐将流量从一个版本转移到下一个版本,直到完全转移到最新版本。但在蓝绿部署中,我们创建了两个不同的环境,一个用于上线,另一个用于暂存新版本。因此,蓝绿部署设置会为暂存和生产创建独立的区域,然后将流量从一个区域路由到另一个区域,同时部署最新版本。

假设我有一个蓝色区域(us-east-1),这是生产区域,并且它是实时的,已部署当前版本的 Lambda 函数。现在假设我也发布了一个新版本,所以我将 Lambda 函数(新版本)部署到绿色区域(us-east-2),它将作为我的暂存环境。我将在此环境中进行所有测试,一旦满意,就将所有流量重定向到绿色区域(us-east-2)。现在我的暂存环境已上线,我将继续将最新版本部署到蓝色区域(us-east-1),并测试我的函数以查找漏洞和问题。但假设不幸的是,在蓝色区域(us-east-1)发现了一些严重的漏洞。

然后,代码被回滚,所有流量再次指向蓝色区域(us-east-1),而绿色区域(us-east-2)再次成为暂存环境:

  • 蓝色:$ serverless deploy --stage prod --region us-east-1

  • 绿色:$ serverless deploy --stage prod --region us-east-2

将 CloudWatch 与 ELK 集成

我已经使用 ELK 很长时间了。它曾是我的日常工作,因为 AWS Lambda 日志会被发送到 CloudWatch,但由于我的公司使用 ELK 来集中管理日志,现在我希望将所有 CloudWatch 日志推送到 ELK。

所以我决定将 CloudWatch 日志传输到 ELK。Lambda 日志可以直接传输到 Elasticsearch 或 Redis,由 Logstash 拿取。有一个插件可以帮助我们将 Lambda CloudWatch 日志传输到 ELK。接下来我们将了解如何配置它。我们将使用 Docker ELK 镜像在本地设置 ELK,然后通过 Logstash 插件连接到 AWS CloudWatch,最后将日志推送到 Elasticsearch。让我们按照以下步骤操作:

  1. 获取 ELK 的 Docker 镜像,如以下代码所示。如果你已经设置了 ELK 账户,则无需执行此步骤:
$ docker pull sebp/elk
$ docker run --rm -it -p 5044:5044 -p 5601:5601 -p 9200:9200 -p 9300:9300 sebp/elk:latest

现在,如果你打开浏览器并访问链接 http://localhost:5601/,你应该能看到 Kibana 仪表板。

  1. 安装 logstash-input-cloudwatch_logs 插件。通过 SSH 登录到你在上一步创建的 Docker 容器并安装插件,如以下代码所示:
$ docker exec -it <container> bash
$ /opt/logstash/bin/logstash-plugin install logstash-input-cloudwatch_logs

Validating logstash-input-cloudwatch_logs
Installing logstash-input-cloudwatch_logs
Installation successful 
  1. 插件成功安装后,我们需要创建一个 Logstash 配置文件,帮助我们将 CloudWatch 日志传输到目标位置。打开编辑器,添加以下配置文件并命名为 cloud-watch-lambda.conf。我们需要根据 AWS IAM 用户替换 access_key_idsecret_access_key,并更新日志组。我添加了三个 grok 过滤器:

    • 第一个过滤器匹配通用的日志消息,其中我们会去除时间戳,并提取出 [lambda][request_id] 字段进行索引

    • 第二个 grok 过滤器处理 STARTEND 日志消息

    • 第三个过滤器处理 REPORT 消息,并提供最重要的字段

[lambda][duration]
[lambda][billed_duration]
[lambda][memory_size]
[lambda][memory_used]
input {
    cloudwatch_logs {
        log_group => "/AWS/lambda/my-lambda"
 access_key_id => "AKIAXXXXXX"
 secret_access_key => "SECRET"
        type => "lambda"
    }
}

我们将其命名为 cloud-watch-lambda.conf 并将其放置在 Docker 容器的 /etc/logstash/conf.d 文件夹中。

  1. 现在让我们重启 Logstash 服务,如以下代码所示。完成后,你应该能看到日志已被拉入我们的 ELK 容器:
$ /etc/init.d/logstash restart
  1. 打开浏览器并访问页面 http://localhost:5601,我们应该能看到 CloudWatch 的日志流式传输到 ELK,并且可以使用 ELK 过滤器和正则表达式进一步优化此过程。

摘要

在本章中,我们学习了如何将 DevOps 与 AWS Lambda 内置的各种工具以及开源工具集成。在下一章中,我们将学习如何通过 Serverless Framework 设置 CI 和 CD,并学习如何在 Azure Functions 中进行监控和日志记录。

第四章:使用 Azure Functions 进行 DevOps

Microsoft Azure 是全球第二大云服务提供商,仅次于 AWS。但直到 2015 年,他们才提供 API 服务,之前在 AWS 和 Google Cloud Functions 等其他云服务领域表现出色。最终,他们决定投资于云函数。所以,Azure Functions 就是 AWS Lambda 的回应,但其底层技术不同,我们将在接下来的章节中详细介绍。Azure Functions 基于 Azure 应用服务和 WebJobs SDK。Azure Functions 支持多种语言,如 F#、Python、Batch、PHP 和 PowerShell,但官方支持的语言是 C# 和 Node.js。我们将在本章节中使用 Node.js 进行所有的教程,并且有关 Azure Functions 的更多详细信息可以参考微软链接:https:/ / msdn. microsoft. com/ en- us/ magazine/ mt793269. aspx。

在本章中,我们将讨论以下主题:

  • 构建一个简单的 Azure 函数

  • 使用 Azure Functions 进行持续集成和持续交付

  • 持续部署到 Azure Functions

  • Azure Functions 的蓝绿部署

  • 监控与日志记录

  • 最佳实践

构建一个简单的 Azure 函数

我们将首先尝试通过门户创建一个简单的 Azure 函数,然后使用 S 工具和框架自动化构建和部署过程。所以,为了开始使用 Azure Functions,我们需要首先创建一个 Azure 云服务账户。微软提供一个月的免费订阅,我们将在所有 DevOps 相关的示例和演示中使用 Node.js 脚本。

Azure 云账户创建

azure.microsoft.com/free/?ref=microsoft.com&utm_source=microsoft.com&utm_medium=docs&utm_campaign=visualstudio

创建账户后,按照以下步骤操作:

  1. 登录 Azure 门户(Azure 门户:portal.azure.com/)。

登录成功后,我们将被重定向到 Azure 门户的主页。

  1. 现在,我们需要为 Azure Function 创建资源。点击“创建资源”按钮,这将把我们带到 Azure 市场页面。

  2. 在这里,我们需要通过点击 Azure 市场中的 Compute 链接来创建一个计算资源。

  3. 然后,我们可以继续通过点击 Function App 链接来创建一个函数(这会打开一个新窗口,我们需要在其中输入 Function App 的详细信息)。

以下截图展示了在创建简单的 Azure 函数时,我们如何在屏幕中进行操作:

  1. 点击链接后,将打开一个页面,我们需要在其中输入有关功能应用的详细信息。所以让我们将它命名为mySampleAppName。功能应用名称必须是唯一的,因为 Azure Function 不允许在 Azure 云中使用重复的应用名称。接受应用名称后,我们会在应用名称列上看到一个绿色的勾号。然后我们将添加订阅、资源组(可以使用默认值或添加现有的资源组)、操作系统、托管计划、存储(我们可以创建新的存储或使用现有存储)。

保留其他细节为默认设置,并创建一个功能应用:

这将创建一个功能应用,功能将在其中运行。成功创建功能应用后,如果点击所有资源,你应该能看到该功能应用。接下来,我们将继续创建应用,之后 Azure 将为该功能创建资源,其中包括存储帐户和功能应用。

  1. 现在让我们去功能应用,在右侧的功能应用标签下,我们应该能看到我们的功能。点击它并将鼠标悬停在上面,我们应该能看到+号。我们可以通过点击+来创建一个新的功能;只有在悬停在功能标签上时,才能看到+号。这将打开右侧的一个标签,其中提供了多个选项来创建 Azure 功能。我们将继续选择场景作为Webhook + API,并选择 JavaScript 作为语言,然后点击创建此功能

点击按钮后,会打开一个 UI,里面嵌入了一个示例 Node.js 功能。我们可以运行同样的 Azure 功能来检查它的表现。

我们可以通过两种方法运行该功能,一种是 POST 方法,另一种是 GET 方法。默认情况下,功能使用 POST 方法运行。所以让我们点击运行按钮。在下一个窗口中,将请求体替换为 JSON 值,然后点击再次运行

在 POST 方法中,让我们添加以下 JSON 详情:

 {
    "name": "My Azure Function"
 }

运行将触发功能并显示请求体中的输出,如下图所示:

而且,当我们将HTTP 方法更改为 GET 时,需要提供一个查询参数。为此,点击+ 添加参数,在查询中填写键为name,值为DevOps,然后点击运行。在成功执行后,我们可以在日志窗口中看到输出日志为Hello DevOps

所以,在之前的教程中,我们学习了如何创建一个简单的 Hello World Azure 功能,并使用 POST 和 GET 方法执行它。在接下来的章节中,我们将探讨如何自动化部署。

使用 Azure Functions 实现持续集成和持续交付

现在,我们通过 Azure 门户创建了一个简单的应用程序,接下来的问题是如何添加多个功能、源代码以及设置自动化构建和部署。当然,我们通过 DevOps 和自动化来完成这些任务。让我们从持续集成开始。正如我们所知,持续集成是 DevOps 的核心部分,在这个过程中,应用程序会集成为一个整体。我们必须通过自动化实现 Azure Functions 的持续集成、自动化测试和持续部署。

首先,我们将创建一条流水线。流水线从代码仓库(如 GitHub 或 SVN)开始,并以生产部署结束。在此过程中的每一环节都必须通过流水线实现自动化,几乎不需要人工干预。我们将在下一章详细讲解流水线的内容。

要实现持续集成,我们需要从将代码推送到 GitHub 仓库开始。开发人员应通过为每个功能模块创建一个特性分支来开始构建应用程序,然后所有的特性分支应当经过审核并合并到开发分支,最终再合并到主分支,以便进行干净的代码构建。所有的特性分支都应该经历持续集成过程,即,功能代码将通过单元测试、状态代码分析以及各种其他自动化测试。然后,特性分支的代码应当合并到主分支。主分支用于构建并部署到 UAT 或 OAT 环境,在那里应进行性能测试,之后再进一步部署到生产环境。

持续交付过程从开发人员将代码提交到本地或特性分支的那一刻开始。自动化构建将被触发,紧接着是单元测试和集成测试。即使代码经过了非常充分的测试,它仍然需要进行可用性和验收测试。因此,成功完成持续集成过程将触发持续交付过程,并将代码交付到 QA 阶段环境。QA 环境通常类似于生产环境。在这里,自动化和手动验收测试将开始执行。通过实现持续交付,我们应该能够每天、每周或每两周发布环境,或根据业务需求进行调整。但我们应该能够不费力或无需手动部署地将代码部署到生产环境。

让我们来看一个遵循持续集成和交付的示例。在这个示例中,我们将使用开源的 DevOps 工具,实际上在 Azure 云上测试和部署应用程序。我们需要像 Jenkins 这样的工具,它是一个流行的开源编排工具,还有我们用于 AWS 部署的无服务器框架,以及用于单元测试代码的 Node.js 模块。

为了验证可行性,我使用 Docker 设置了带有 Node.js 和 npm 的 Jenkins 实例。我已经将这个 Dockerfile 推送到了 GitHub 上的 Git 仓库中,所有示例文件也托管在这个仓库中,以下是我的教程链接,并且我的教程是基于 Linux 系统构建的:github.com/shzshi/azure-helloworld-ci.git

如果你使用的是 Git 仓库中的 Dockerfile,那么确保你的笔记本电脑/PC 上已经安装了 Docker。我们将首先克隆前面提到的 Git 仓库,并使用以下命令构建 Docker 镜像,镜像中包括 Jenkins、Serverless Framework 以及 Azure Functions 部署所需的所有依赖项:

$ git clone https://github.com/shzshi/azure-helloworld-ci.git
$ cd azure-helloworld-ci
$ docker build -t chapter4jenkins:latest .

然后使用以下命令运行 Docker 容器,这将创建一个包含 Jenkins、Node.js 8.9、Serverless Framework 2.5 和 npm 5.6 的容器:

$ docker run --rm -d -p 50000:50000 -p 8080:8080 chapter4jenkins:latest

如果你已经设置了 Jenkins,那么确保服务器上已安装 Node.js 8.9、npm 5.6 和 Serverless Framework 2.5,以便上述示例能正常工作。

一旦 Jenkins 启动并运行,我们将创建一个 Jenkins 作业,它将运行单元测试,并通过 Serverless Framework 将 Azure Function 部署到 Azure 云。成功部署后,我们将通过 Serverless Framework 调用该函数。

按照以下步骤操作:

  1. 以下是 Jenkins 首页的截图。我们将创建一个新作业,点击链接 “New Item” 来创建一个新作业:

  1. 创建一个作业名称,随便取个名字,选择“Freestyle 项目”并提交 OK:

  1. 然后我们将跳转到作业的配置页面,在这里我们将添加用于克隆的 Git 仓库,点击 Source Code Management 标签,并按如下所示添加 Git 仓库的详细信息:

  1. 然后点击 Build 标签页并创建一个执行 Shell,通过 Add build step 下拉菜单,添加 npm install 来从互联网安装所需的 Node.js 模块,然后运行 Node.js 测试,这将单元测试 Azure 库功能并给出结果。

在将应用部署到 Azure 云之前,我们可以添加许多单元测试来测试我们的功能,以确保应用在进入云端之前已通过单元测试:

确保你已经通过 Jenkins Home | Manage Jenkins | Manage Plugins 安装了 Publish HTML report 插件。

  1. 一旦运行了上述作业,就会执行单元测试并生成报告,我们可以通过配置构建后操作来查看该报告。所以,让我们点击 Post-build Actions 标签页,然后点击下拉菜单 Add post-build action 并选择 Publish HTML reports,点击 Add,然后按照以下截图进行配置:

  1. 保存该任务并运行构建。构建成功后,我们应该能够在任务主页上查看代码覆盖率的 HTML 视图。覆盖率会像下面的截图一样,给我们一个关于单元测试执行情况的良好视图:

在上一节中,我们对函数进行了单元测试,并查看了函数的覆盖率。接下来,我们将配置部署到云端。

与 Azure Functions 的持续集成

现在,进入部署到 Azure Cloud Functions 的步骤,我们将使用一个无服务器框架。框架将完成所有繁重的工作,但它需要一些前置条件才能正常工作。它们是:serverless-azure-functionscli az login,用于获取所需的凭据和账户详情。请按照以下步骤在先决条件部分进行操作:

先决条件

我们可以通过 npm 安装serverless-azure-functions。以下命令将安装插件的最新版本,但如果你使用的是上面创建的 docker 镜像,它已经包含了这个插件:

$ npm i --save serverless-azure-functions

但是,为了与 Azure 平台进行交互,我们需要在本地设置 Azure 订阅凭据。为了使serverless-azure-functions插件正常工作,我们需要设置一个服务主体,可以通过 Azure 门户网址完成,如下所示:

docs.microsoft.com/zh-cn/azure/azure-resource-manager/resource-group-create-service-principal-portal

然而,如果你使用的是 PowerShell cmdlets,请使用以下命令:

docs.microsoft.com/zh-cn/azure/azure-resource-manager/resource-group-authenticate-service-principal

我将使用 CLI 进行教程,但你可以自由选择最合适的方式。或者通过 Azure CLI,请按照以下步骤在本地获取 Azure CLI:

docs.microsoft.com/zh-cn/cli/azure/install-azure-cli?view=azure-cli-latest

或者,你也可以使用 Azure Cloud Shell,如下所示:

docs.microsoft.com/zh-cn/azure/cloud-shell/overview

一旦 Azure CLI 成功安装,我们应该能够通过以下命令访问login

$ az login

上一个命令会提示我们访问https://aka.ms/devicelogin并提供一个代码和 Azure 身份。然后我们就可以通过 CLI 访问我们的账户。我们将在命令行中看到如下的账户详情:

{
  "cloudName": "AzureCloud",
   "id": "c6e5c9a2-a4dd-4c81b4-6bed04f913ea",
   "isDefault": true,
   "name": "My Azure Subscription",  
   "state": "Enabled",
    "tenantId": "5bc108159c-4cbe-a7c9-bce05cb065c1",
    "user": {
      "name": "hello@example.com",
        "type": "user" 
    }
 }

我们还需要通过以下命令获取订阅详情:

$ az account list

接下来,我们需要创建一个服务主体。我们可以通过运行以下命令来完成。这将提供一个 JSON 对象,我们需要使用该对象进行 Azure 云身份验证:

$ az ad sp create-for-rbac
 Retrying role assignment creation: 1/36
 {
   "appId": "e28115c2-ba87-4ddb-b3fb-62c91ade4a2e",
   "displayName": "azure-cli-2018-01-30-01-28-48",
   "name": "http://azure-cli-2018-01-30-01-28-48",
   "password": "e87a0489-63a7-41a1-b0ef-9054eda5b8c8",
   "tenant": "0a76ffdc-0cda-4d93-850f-025f940889dc"
 }

最后一个前提是需要在 Jenkins 中安装EnvInject插件来掩盖 Azure 凭据。请确保已经添加该插件。

设置环境变量

最后,我们创建环境变量,包括订阅 ID、租户、名称和密码。我们需要将其导出到 Jenkins 节点,并设置部署。因此,让我们来完成这一步:

  1. 在浏览器中打开 Jenkins,点击新建项目添加一个新作业。输入项目名称myAzureFunctionDeploy并选择自由风格项目,然后点击确定

  2. 在工作配置页面,勾选此项目是参数化的,并在名称下方添加密码参数,默认值通过以下 Azure 命令获取:

azureServicePrincipalTenantId
azureServicePrincipalClientId
azureServicePrincipalPassword
azureSubId
  1. 点击源代码管理标签,添加仓库 URL:github.com/shzshi/azure-helloworld-ci.git,该仓库包含所需的文件。

  2. 构建环境中选择将密码注入为构建环境变量。我们这样做是为了掩盖 Azure 凭据和其他详细信息。

  3. 接下来,点击构建标签,在执行 Shell 中添加以下示例中的步骤,然后点击保存

export azureSubId=${azureSubId}
export azureServicePrincipalTenantId=${azureServicePrincipalTenantId}
export azureServicePrincipalClientId=${azureServicePrincipalClientId}
export azureServicePrincipalPassword=${azureServicePrincipalPassword}
serverless deploy
  1. 点击使用参数构建。然后点击构建,作业将构建,创建一个包并将函数部署到 Azure 门户。我们可以在云端测试该函数。函数部署后,我们可以通过无服务器命令提示符调用或运行该函数。我们可以将其集成到冒烟测试或功能测试中,因此我们可以通过命令行或将相同的命令行添加到 Jenkins 作业中来测试函数的实际运行,如下所示:
$ serverless invoke -f hello --data {"name": "DevOps"}

我们还可以流式传输函数的日志,以查看函数如何被部署,或者它在运行时的表现:

$ serverless logs -f <function_name>

我们可以修改源代码仓库中的代码,并单独运行这些命令,或者通过像 Jenkins 这样的编排工具来实现完美的持续集成和持续交付。

我们可以通过以下命令集成从 Azure 函数中移除该函数。这可以集成到 Jenkins 管道中,以取消部署或将函数回滚到先前版本:

$ serverless remove

持续部署到 Azure 函数

持续部署意味着我们每次提交的代码都应经过自动化的单元测试、集成测试和性能测试,并且经过自动化的源代码分析后,能够成功部署到生产环境中,而无需人工干预。然而,在某些情况下,我们应能够因生产环境中的一些错误或问题回滚部署。回滚可以是自动化的,但通常是手动执行的。我们可以通过 Azure 门户设置一个持续交付的流水线。让我们设置一个简单的持续交付流水线。Azure Cloud 提供了一个开箱即用的功能来设置 Azure Function 的流水线。它与多种源代码管理工具集成,如 Git、GitHub、Bitbucket、Visual Studio Team Services 等。持续部署是按每个函数应用设置的,函数代码也可以通过源代码仓库进行管理,门户中的代码变为只读。因此,一旦代码更新并提交到源代码仓库,函数代码会自动构建并部署到门户。

设置一个持续部署的 Azure 部署工具

在这里,我们将演示如何通过 Azure 门户设置与 GitHub 仓库的持续交付:

  1. 登录到 Azure 门户 (portal.azure.com)。登录成功后,点击“所有资源”链接。我们应该能看到已经创建的函数应用:

  1. 选择我们需要设置持续部署的应用程序,根据我的示例,我会选择 azure-helloword-ci,这将带我们进入函数应用窗口。然后,在函数应用窗口中,我们会选择右上角的“平台功能”标签页,点击“部署选项”:

  1. 然后,在部署部分,点击“设置”。接下来会出现一个窗口,标题为“部署选项”。点击链接“选择源”,在下一个窗口中,我们将看到添加源控制的选项。选择 GitHub。

  2. 接着我们将看到配置 GitHub 仓库的选项,我们需要授权 Azure Functions 访问我们的私人或公共仓库。请确保使用自己的账户的公共仓库。

  3. 配置完成后,我们需要选择仓库,填写选择源、授权、选择组织、选择项目仓库、分支,并可以配置性能测试。我们将被引导到 GitHub 门户,输入我们的 GitHub 凭证以连接到 GitHub 仓库。之后,一旦点击确认,仓库将与 Azure Function 配置,并且 GitHub 中的所有文件更改都会被复制到函数应用中,触发完整站点部署。

  4. 我们可以通过点击配置性能测试按钮来设置性能测试。我们可以通过创建不同的分支来创建多个环境,然后将代码合并到主分支以进行生产部署。

Azure Functions 中的蓝绿部署

DevOps 中有多种部署模式:金丝雀部署和蓝绿部署。在本书中,我们将讨论 Azure Functions 的蓝绿部署。蓝绿部署技术通过使用两个相同的生产环境(分别命名为蓝色和绿色)来减少停机时间和风险。在这种模式下,一个环境用于线上运行,另一个用于新更改的预发布。在切换流程中,初始版本的应用程序部署到蓝色环境,所有用户流量都将重定向到蓝色环境。之后,新的应用程序版本被部署到预发布环境(绿色环境)进行测试。当新的软件版本测试合格后,所有流量将切换到绿色环境,并认为该环境为正式生产环境。

部署仪表板

开发团队需要始终跟踪部署情况和开发流水线的健康状态。它还需要跟踪部署失败的情况、已完成的发布次数、代码提交次数以及许多对开发周期至关重要的其他指标。有一个开源工具可以帮助跟踪所有这些细节,它就是 Hygieia。Hygieia 是一个开源工具,可以在单一界面上提供构建、部署、质量控制和应用性能的整体视图。它还帮助跟踪部署版本以及应用程序的健康状况。

Hygieia是由 Capital One 开发的 DevOps 仪表板工具,该公司已将其开源。

Hygieia 仪表板有两种视图,一种是小部件视图,另一种是流水线视图。小部件视图显示当前冲刺中的功能、代码贡献活动、持续集成活动、代码分析、安全分析、单元和功能测试结果、部署以及环境状态的信息。流水线视图显示部署生命周期的各个组件,展示从开发、集成、QA、性能到生产的进展。Hygieia 还提供产品仪表板,显示各个产品中多个应用的协作仪表板。

Hygieia 有许多插件,可以与 Jira、Subversion、Jenkins、Sonar、IBM UrbanCode Deploy 等多个 DevOps 工具集成或获取日志。由于 Hygieia 是开源的,我们还可以为其构建自己的插件和小部件。

Hygieia 仪表板应用程序开箱即用,能够与 VersionOne、Jira、Subversion、GitHub、Hudson/Jenkins、Sonar、HP Fortify、Cucumber/Selenium 和 IBM Urbancode Deploy 集成。

Hygieia 基于 Java 构建,插件通过 Java 命令行运行。我们可以在 Docker 容器中搭建完整的 Hygieia 仪表盘。请访问以下链接进行设置和集成:github.com/capitalone/Hygieia

监控和日志记录

无服务器应用程序有助于加速开发和提高独立性,并允许触发事件的执行,但当这些功能未按预期工作时,我们如何验证事件是否正在激活正确的功能?在无服务器应用程序中,根本原因分析变得更加复杂,因为服务很小,功能也非常精确。在追踪故障源时,涉及的服务或任何集成点实际上并不存在,而且当有多个功能参与操作时,调查变得非常困难。

这就是日志发挥至关重要作用的地方,但在无服务器架构中进行日志记录时,有一些独特的考虑因素需要加以注意。

多个功能失败并未提供请求的功能,或者日志包含一个统一的事务标识符,这在无服务器应用中是正常现象,因此,当分析功能日志时,可以轻松检测到事务失败,并修复故障问题。

在 Azure Functions 中,我们可以通过多种方式获取日志。最简单的方法是登录到 Azure 门户,进入相应的功能并选择“监视”选项卡。在这里,我们将看到一列功能调用记录、它们的状态,以及最后一次运行的时长。如果我们选择某个失败的调用,则在右侧可以看到准确的日志和其他相关信息。

通过 Kudu 访问日志

我们可以通过 Kudu 访问日志。Kudu 是为访问 Microsoft Azure Web 应用环境而开发的。由于 Azure Function 运行在 Web 应用中,我们可以使用 Kudu 来访问 Azure Functions 的日志。Kudu 可以访问环境信息,并且还可以查看运行时附加到功能的实际文件系统的详细信息。

可以通过 Web 浏览器访问 Kudu。相关链接是 myAzureFunction.scm.azurewebsites.net。我们只需要将 myAzureFunction 替换为我们的 Web 应用名称。Kudu 的最大优点是,它可以通过 Shell 或浏览的方式让我们访问功能环境的实际文件系统,这使得追踪故障根源变得更容易。

通过表存储记录日志信息

所有函数的日志和时间信息都存储在我们之前创建函数应用时所创建的存储帐户中。如果我们安装了 Azure SDK,可以通过 Visual Studio 访问日志。我们还可以通过 Cloud Explorer 窗口访问日志。(如果你有多个 Azure Function 应用,可能有点难记住哪个属于哪个函数应用,但你可以通过查看应用程序设置中的 AzureWebJobsStorage 设置内容来找到它)。

监控 Azure Function

监控函数非常重要。它能清楚地展示我们的函数或应用在处理流量时的表现,以及在扩展和收缩时的表现。Azure Function 与 Azure 应用程序洞察内置集成。应用程序洞察是由 Azure 提供的 APM(应用程序性能管理)服务。我们可以在创建函数应用时配置应用程序洞察,并将遥测数据发送到应用程序洞察。创建一个新的函数应用,打开应用程序洞察开关并设置应用程序洞察位置:

我们可以在 Azure Cloud 门户的 Metrics Explorer 中查看性能图,以监控函数的健康状况:

与 New Relic 集成

New Relic 提供了 Azure Function 的集成,执行以下功能:

  • 将 Azure Functions 的数据报告给 New Relic 产品。

  • 报告度量数据,如执行的函数数量、发送和接收的字节数以及 HTTP 错误。

  • 收集有关服务状态和配置的库存数据。

你可以通过 New Relic 监控并对 Azure Functions 数据设置告警,还可以创建自定义查询和仪表盘。

要激活 Azure Functions 集成,请通过以下链接进行 Azure 集成激活:docs.newrelic.com/docs/integrations/microsoft-azure-integrations/getting-started/activate-azure-integrations

最佳实践

面向无服务器的 DevOps 仍然是一个相对年轻且不断发展的领域。无服务器本身正在快速崛起,而在这个框架内,为了实现与其他 DevOps 的自动化部署和集成,变化速度更快。谈到从开发到生产的顺畅代码部署的最佳实践时,我们需要关注 Azure Function 的 DevOps 的各个要素,并从实施过程中吸取经验教训。但由于无服务器技术仍在发展中,且无服务器的 DevOps 仍属于小众领域,我们可能无法涵盖 DevOps 的所有方面。不过,我们将重点关注其中的一些关键要素,并在这里详细讨论。因此,面向无服务器的 DevOps 的主要要素包括:源代码管理、构建、部署、发布管理、监控和日志记录,这几乎与应用程序开发相同。但由于无服务器是微服务架构,因此在将 DevOps 应用到它时有所不同。接下来,我们将探讨每个方面,并学习如何将 DevOps 最好地应用于 Azure Functions。

源代码管理

开发人员编写代码并共同协作执行任务,但开发人员不断添加新功能,或者更新代码以提高性能,或增强功能性。这就是我们需要源代码管理的原因。Git 是目前最流行的源代码管理工具之一。因此,我们必须为高效管理代码设定不同的策略。但在此之前,我们需要为 Azure Function 设置一个文件夹结构。

文件夹结构

特定功能应用程序的所有函数代码将位于根文件夹中,可能有一个或多个子文件夹,并且包含一个主机配置文件。host.json 文件将包含运行时特定的配置。每个函数文件夹将包含一个或多个代码文件、function.json 配置文件以及其他函数依赖项。

wwwroot
| - host.json
| - myazurenodefunction
| | - function.json
| | - index.js
| | - node_modules|
| | - ... packages ...
| | - package.json
| - myazurecsharpfunction
| | - function.json
| | - run.csx

在文件夹结构中,我们需要应用最好的分支策略,以实现稳健的开发和顺畅的部署。最佳实践是拥有多个分支,其中 master 分支是默认分支,方便开发,并便于未来顺利发布到生产环境。这些分支包括功能分支、开发分支、发布分支和热修复分支。

功能分支有助于轻松跟踪某个功能,并促进团队成员之间的并行开发。功能分支可能从开发分支派生,但应始终合并回开发分支。下一个主要分支是开发分支,该分支贯穿项目的开发和支持生命周期。当开发分支的源代码稳定时,它会被合并回 master 分支,成功发布后,每次合并到 master 分支都会触发生产环境的发布。

发布分支可能来自开发分支,但它们必须合并回开发和主分支。发布分支用于准备新的生产发布。发布分支用于下一个大版本发布。它为我们提供了进行次要错误修复的空间,并为发布设置元数据。当发布分支准备好进行实际发布时,我们需要确保它与主分支合并,并且提交必须被标记为未来的历史版本。最后,发布分支应与开发分支合并,以便我们在未来发布中获得此发布的所有更改。

最后是热修复分支。这与发布分支几乎相同,因为它帮助我们进行生产发布。当生产部署失败时,热修复实际上非常有用,因此我们可以快速修复生产问题,以便及时恢复生产。为了解决生产故障,我们从主分支的生产标签中分支出热修复分支,这样一个团队可以修复生产故障,而另一个团队可以继续在开发分支上开发。一般的经验法则是热修复分支可能从主分支分支出去,一旦热修复成功,变更将合并回主分支。它们还需要合并到开发分支,以确保我们在未来发布中拥有这些热修复变更。

测试和静态代码分析

测试是开发的重要组成部分,当我们开发任何应用程序时,测试是必须的。它有助于在进入生产之前减少大部分错误和性能问题。在应用程序开发中涉及许多类型的测试。这些包括单元测试、集成测试和性能测试。它们集成到不同的环境部署阶段中。单元测试集成在开发阶段中。

无服务器处理程序应始终是一个使用代码库中的模块的薄层。模块应该有很好的单元测试覆盖率;然后在集成测试期间测试无服务器应用程序将会更加容易。

我们应该尝试在本地运行单元测试和集成测试模块和函数,这样可以帮助更快地运行测试,并且更容易在代码库中找到问题,而无需部署到云端。我们也应该远程运行它们,因为远程基础设施与本地基础设施有所不同。我们可以通过设置流水线并将它们分阶段部署到不同的环境(开发、阶段、用户验收测试、预生产和生产),其中开发和阶段测试应在本地调用,而其他环境则通过远程测试以获得更好的性能。

静态代码分析是分析源代码,以标记编程错误、漏洞、风格错误和可疑构造的过程。我们必须确保在管道阶段集成服务器函数的 linting 工具。这有助于保持代码整洁、减少漏洞,并确保正确缩进。

部署和发布

部署模式有多种类型,但我们应选择一个适合我们情况的模式,并将所有功能一次性组合使用:蓝绿部署和金丝雀发布,并且还涉及诸多复杂性,如冗余、高可用性、回滚、A/B 测试和增量发布。所有这些都需要在部署过程中考虑,部署应实现轻松和灵活。因此,在开始自动化之前,最好先决定使用的部署模式。

我们必须确保部署始终被监控,并且随着时间的推移,对部署失败进行分析和改进。所有部署应通过自动化管道触发,尽量减少人工干预,并附加到 GitHub 仓库,采用严格的分支策略。我们还可以将函数应用打包并上传到 Nexus,以便更轻松地回滚。所有部署都应该有一个反馈环,直到项目追踪工具如 JIRA/rally。这将使故障追踪变得更加容易。

总结

总结一下,我们涵盖了 Azure Functions 的持续集成、持续部署、日志记录和监控,同时也详细讨论了如何在源代码仓库中管理 Azure Functions 的最佳实践,测试和自动化部署的重要性。无服务器或基于 Azure 云的函数仍处于初期发展阶段,但随着每一天的过去,它们在不断改进。就采纳和改进而言,仍然有很多工作要做。但一旦无服务器的使用增多,围绕它的 DevOps 实践也会逐渐改进并变得更加健壮。在下一章中,我们将学习如何将 DevOps 应用于 OpenWhisk,另一个无服务器提供商。

第五章:将 DevOps 与 IBM OpenWhisk 集成

本章中,我们将介绍 OpenWhisk,它是一个开源的云端无服务器提供商。我们还将学习如何构建、测试和部署 OpenWhisk 函数,并了解如何管理日志和监控函数。此外,我们还将探讨与 OpenWhisk 一起使用 DevOps 的最佳实践。

OpenWhisk

OpenWhisk 是 Apache 和 IBM 的一个孵化器项目。它通过 IBM Bluemix 门户提供一个开源的云端无服务器平台。我们可以在本地部署或直接使用云端模式。OpenWhisk 的商业版由 IBM 提供,通过 Bluemix 发布,开源版则可以作为本地部署的基础设施即服务IaaS)提供,也可以在云端(如 Bluemix、Amazon EC2、Microsoft Azure 或 GCP)使用。开源版本可在 GitHub 上获取,并通过 Apache 许可证公开,用户可以为其贡献代码。

要查看 GitHub 上的开源 OpenWhisk 版本,请访问 github.com/apache/incubator-openwhisk/tree/master/docs#getting-started-with-openwhisk

像其他任何无服务器服务一样,OpenWhisk 执行的是小段代码,这些代码被称为动作(即,函数),在事件触发时执行。事件可以来自 Bluemix 或外部来源。OpenWhisk 还声称它允许集成自家事件提供者,或任何其他事件提供者,而不像其他无服务器服务提供商那样只限于特定的事件提供者。

OpenWhisk 提供了许多功能,例如自动扩展和负载均衡,开箱即用,无需手动配置集群、负载均衡器和 HTTP 插件。让我们看看 OpenWhisk 的架构图:

动作/函数是在事件触发时执行的,这些事件可以是数据库记录的变化、物联网传感器读数、GitHub 仓库的代码提交,或来自 Web 或移动应用程序的简单 HTTP 请求。这些来源的事件通过触发器传递,并且触发器中的规则允许动作对这些事件作出响应。

动作是 OpenWhisk 中对函数的称呼。动作可以是小段的 JavaScript 或 Swift 代码,或者是嵌入在 Docker 容器中的自定义二进制代码。

动作通常是按顺序调用的,一个动作的输出可以作为下一个动作的输入。除了触发器,动作还可以通过多种来源调用,例如 OpenWhisk API、CLI 和 iOS SDK。

OpenShift 支持的语言有 Node.js、Python、Swift、Java 和 PHP,OpenShift 声称其中一个独特卖点是支持 Swift 语言,这在为移动应用编写函数或动作时非常有用。

服务和事件提供商的集成可以通过包来执行。是一个包含了多个 feed 和操作的捆绑包。Feed 是一段代码,用于配置外部事件源以触发事件。例如,在银行账户交易中,当发生借记或贷记事件时,会创建一个触发器,触发一个函数/操作来向用户发送关于交易的短信。包中的操作包含可重用的逻辑,服务提供商提供这些逻辑供用户使用。这些服务提供商是事件源,并且可以通过 API 被调用。

OpenWhisk 构建在 NGINX、Kafka、Docker 和 CouchDB 上。这些组件共同作用,提供一个为 OpenWhisk 提供无服务器服务的架构。

OpenWhisk 设置与配置

OpenWhisk 运作四个重要的概念,具体如下:

  • 触发器

  • 操作

  • 规则

  • 序列

触发器

触发器就像由数据源创建的事件。一些例子包括更改数据库记录、向你的仓库提交新代码,以及来自 Web/移动应用的 HTTP 请求。来自这些源的事件通过触发器进行传递。

操作

操作是用 Node.js 或 Swift 编写的代码片段,或嵌入在 Docker 容器中的代码。代码被部署并在触发器触发时执行。

规则

规则 是触发器的一部分,当与操作结合时,每次触发器触发时,规则都会调用相应的操作。

序列

操作链称为 序列。我们可以将操作串联起来,它们会按顺序依次被调用。一个操作的输出可以成为下一个操作的输入。

构建一个 OpenWhisk 应用程序

在本书中,我不会设置 OpenWhisk 的开源版本。相反,我们将通过 Bluemix 门户使用教程,因此我们需要注册 Bluemix 门户。Bluemix 为新用户提供一个免费的一个月订阅。所以让我们在 bluemix.net 上注册。正如我之前提到的,IBM Bluemix 提供了一个 免费试用 ,前 30 天无需提供信用卡信息。试用期后,我们不需要使用信用卡注册。所有用户每月都可以享受约 400,000 GB-秒的无服务器计算时间。额外的执行时间将按每 GB-秒 $0.000017 收费,按最接近的 100 毫秒进行四舍五入。

创建一个 Hello World OpenWhisk 操作

本教程的前提条件是一个 IBM 云账户(免费试用)以及 Node.js 的知识(本书中我们一直在使用)。我们将首先从一个简单的示例开始,然后随着章节的进行逐步将自动化应用于教程代码。所以,让我们在 IBM Bluemix Cloud 的用户界面上创建一个简单的 Hello World 应用,并执行/调用它。

要创建一个 OpenWhisk 操作,请按照以下步骤进行:

  1. 进入 IBM 云中的 OpenWhisk 控制台 console.bluemix.net/openwhisk/

  2. 登录到 IBM Cloud,点击开始创建,在浏览器中使用 OpenWhisk 并进入编辑器。

  3. 从多个选项中点击创建动作

  4. 使用以下参数创建一个新的动作:

字段
动作名称 HelloWorld
封装包 (默认包)
运行时 Node.js 6
  1. 点击创建。这将打开 Node.js 的编辑器界面。

  2. 默认情况下,OpenWhisk 动作是 JavaScript。它们接收一个关联数组作为输入,并返回另一个关联数组作为输出。

  3. 将现有的 Node.js 代码替换为以下内容:

/**
  *
  * main() will be invoked when you Run This Action.
  *
  */
function main(params) {
    var myName;  
    myName = params.name;
    if (myName == undefined)
        myName = "";     
    return {
        html: "<b>" + JSON.stringify(params) + "</b>",
        js: "alert('hello " + myName + "');"
    };
}
  1. 点击保存按钮。这将保存动作并将你重定向到调用动作的页面。

  2. 让我们点击更改输入并添加以下名称输入,然后点击应用按钮:

{   
 "name": "Serverless"
}
  1. 现在让我们点击调用按钮来执行动作/函数。我们应该能看到以下输出:

输出将以 JavaScript 和 HTML 的形式展示。在前面的教程中,我们创建了一个 OpenWhisk 动作并通过 Bluemix 控制台进行部署。在下一节中,我们将使用 Serverless Framework 来设置部署和自动化。

使用 Serverless Framework 的 OpenWhisk

要使用 Serverless Framework 进行部署设置,我们首先需要准备一些东西。因此,考虑到你已经注册了,我们需要通过 Bluemix CLI 检索 Bluemix 端点和密钥,首先我们需要设置和配置 Bluemix CLI,可以通过 console.bluemix.net/openwhisk/learn/cli 完成。

一旦 CLI 设置完成,我们将通过 Bluemix CLI 安装 Cloud Function 插件,然后登录到 Bluemix Cloud 并部署该动作。请按以下步骤操作:

  1. 首先,我们使用以下命令从 Bluemix 门户获取端点和认证密钥:
$ bx wsk property get --auth whisk auth 
00700a7f-2b1a-4831-bf323a566263ed44:4TBcM7f8g0gj5UPgSVHXwNmMkfpbX36OdWximngOwqZYAJrDSkZwPjeSPjQ45Wm1
  1. 一旦通过上述命令获得认证详情,我们可以通过wsk命令行设置--apihost--auth,如下所示:
$ wsk property set --apihost OpenWhisk.ng.bluemix.net --auth 00700a7f-2b1a-4831-bf32-3a566263ed44:4TBcM7f8g0gj5UPgSVHXwNmMkfpbX36OdWximngOwqZYAJrDSkZwPjeSPjQ45Wm1

ok: whisk auth set. Run 'wsk property get --auth' to see the new value.
ok: whisk API host set to OpenWhisk.ng.bluemix.net

获取和设置密钥的另一种方式是通过门户。我们还可以通过bashrc为框架配置凭证以供使用。要做到这一点,首先通过浏览器登录到 Bluemix 门户,进入 API 密钥部分以检索并导出它们。因此,让我们登录到 Bluemix 门户 console.bluemix.net/openwhisk/,然后在左侧栏选择 API 密钥菜单以获取认证密钥,如下图所示:

  1. 让我们进入 Linux 或 macOS 终端,并使用以下代码将认证详情添加到用户配置文件中:
$ vi ~/.bashrc
  1. 从 Bluemix 门户复制以下导出,包含正确的authapihost
export OW_AUTH=00700a7f-2b1a-4831-bf32-3a566263ed44:4TBcM7f8g0gj5UPgSVHXwNmMkfpbX36OdWximngOwqZYAJrDSkZwPjeSPjQ45Wm1
export OW_APIHOST=OpenWhisk.eu-gb.bluemix.net
  1. 下一步是安装 Serverless Framework 以及相应的依赖和提供程序插件,如下代码所示:
$ npm install --global serverless serverless-openwhisk

插件必须作为全局模块安装才能正常工作。

一个简单的 OpenWhisk 应用

现在让我们通过以下步骤,使用无服务器 Node.js 模板创建一个简单的服务:

  1. 创建一个新的无服务器服务/项目:
$ serverless create --template openwhisk-nodejs --path new-service 
  1. 进入新创建的目录:
$ cd new-service
  1. 安装 npm 依赖:
$ npm install
  1. 完成后,将服务和函数部署到 Bluemix 门户:
$ serverless deploy -v
  1. 然后我们调用函数,检查函数是否正确部署。使用以下代码调用无服务器应用:
$ serverless invoke --function hello
 {
 "payload": "Hello, World!"
 }

在前面的教程中,我们获取了 auth 密钥,创建了一个动作,并通过 Serverless Framework 部署了该函数。在下一部分,我们将探讨通过不同的方法设置 CI 和 CD。

使用 OpenWhisk 进行持续集成和持续交付

对于部署,我们通常会使用 wsk 命令行。我们将使用它执行以下操作:

  • 从 GitHub 拉取代码

  • 设置 Bluemix 空间以运行应用

  • 为应用程序创建服务以供运行

  • 配置环境变量,如服务凭证

  • 部署 OpenWhisk 触发器和动作

  • 推送 Node.js 应用

我们可以使用 IBM Bluemix 工具链,也可以使用开源工具,如 Jenkins 和 Serverless Framework。我们先来看 Bluemix 工具链。

设置工具链和仓库集成

让我们通过以下步骤创建一个新的空白工具链。这可以很方便地从 Bluemix 首页console.bluemix.net/devops/完成:

  1. 访问上述 URL,然后点击创建工具链。接下来选择“构建你自己的工具链”模板,如下图所示,给工具链命名,然后点击“创建”。完成!

  1. 你将被重定向到“构建你自己的工具链”页面;填写相关信息并点击“创建”按钮。

  2. 一旦工具链创建完成,点击添加工具。由于目的是在代码推送到 Git 时进行部署,首先要添加的是 GitHub 集成。所以我们选择 GitHub,然后按照步骤操作。你将被提示授权一个 GitHub 账户,并提供之前克隆的仓库 URL。完成后,你的 GitHub 仓库(以及如果你勾选了问题选项,则为其问题)的条目将出现在工具链中,然后我们将添加部署部分,以响应我们 GitHub 仓库中的更改。

配置部署

接下来,再次点击“添加工具”,将一个新的条目放入管道中,然后点击“交付管道”添加工具,如下图所示:

一旦交付管道工具就位,点击它,然后继续点击“添加阶段”。顶部有三个标签——输入、作业和环境属性。这些控制着部署活动本身的设置。以下是这些标签的简要概述:

  • 输入:这只是管理使用哪个代码库和分支,以及是否手动运行部署或根据需要进行更改。

  • 环境属性:这是管理我们在设置脚本中可以使用的变量。可以添加纯文本和秘密字段。我们可以在这里设置需要的值,例如数据库凭证、访问令牌等。这必须包括一个 Bluemix API 密钥,可以通过使用bx iam api-key-create keyname生成。在我们的例子中,我们需要创建一个名为APIKEY的变量,如下代码所示:

 $ bx iam api-key-create myFunction
 Creating API key myFunction as user@email.com...
 OK
 API key myFunction was created
 Please preserve the API key! It cannot be retrieved after it's
 created.
 Name myFunction
 Description
 Created At 2018-02-24T19:16+0000
 API Key X4klg0XvNotJqN_haPWpPirPLhckCA9OFdZDufjcIxLY
  • 作业:这是实际完成工作的标签。我们将使用一个作业,所以点击“添加作业”,输入“部署”,在部署页面上,我们保持大部分细节为默认设置,确保帐户、组织和空间信息正确无误。

以下部署作业的 Shell 脚本将获取命令的 Cloud Functions 插件(更多关于 Bluemix 命令行工具的信息,请参见文档console.bluemix.net/docs/cli/reference/bluemix_cli/get_started.html#getting-started),然后使用我们之前配置的 API 密钥进行登录,并定位到所需的 Bluemix 组织和空间。将以下 Shell 脚本添加到 JOBS 的部署脚本列中,然后点击保存:

#!/bin/bash
bx plugin install Cloud-Functions -r Bluemix -f
#bx login -a https://api.ng.bluemix.net --apikey $APIKEY -o <email_address> -s development
bx login -a api.eu-gb.bluemix.net --apikey $APIKEY -o <email_address> -s dev
# handy for debugging
bx wsk action invoke /whisk.system/utils/echo -p message helloWorld --blocking --result
# now set up your actions etc
zip helloWorld.zip index.js
bx wsk action update helloWorld --kind nodejs:6 helloWorld.zip
# check everything is as expected
bx wsk action list

在将 Cloud Functions 插件添加到 Bluemix 命令行工具后,这个脚本会使用我们在配置部署工具时创建的 API 密钥进行登录。使用内置的/whisk.system/utils/echo操作将显示我们在日志中的输出,如果一切配置正确以便与 Cloud Functions 一起工作,否则会显示(希望是有帮助且有信息量的)错误。action update命令执行实际的部署,获取新压缩的文件并将其作为一个动作部署。最后一次调用action list只是简单地展示我们预期的动作已经存在。

通过检查此任务上的绿色播放按钮,确保一切按预期工作,如下图所示。如果它播放,说明一切就绪!

使用无服务器框架的持续集成和持续交付

之前,我们使用 IBM Bluemix 工具设置了持续部署。在这里,我们将使用开源工具为 OpenWhisk 函数设置持续集成和持续交付。我将设置一个安装了 Serverless Framework 的 Jenkins 容器。我已经创建了一个 Dockerfile,通过它可以轻松创建一个包含 Node.js、Serverless Framework 和 OpenWhisk 插件的 Jenkins 容器。我已将这个 Dockerfile 添加到 Git 仓库中。我们将使用天气报告 OpenWhisk 应用程序,进行编码,提交到 GitHub,然后通过 Serverless Framework 将其部署到 Bluemix 云。我们将对应用程序进行单元测试,并通过调用函数来运行冒烟测试(见 github.com/shzshi/OpenWhisk-weather-report-serverless.git)。让我们更详细地看一下这些步骤:

  1. Git 克隆仓库:
$ git clone https://github.com/shzshi/OpenWhisk-weather-report-serverless.git
  1. 一旦成功克隆,我们需要使用以下代码构建 OpenWhisk 镜像:
$ docker build --rm -f OpenWhisk-weather-report-serverless/Dockerfile -t OpenWhisk-weather-report-serverless:latest OpenWhisk-weather-report-serverless
  1. 一旦 Docker 镜像构建完成,我们将使用以下代码为 OpenWhisk-weather-report-serverless:latest 镜像创建一个容器:
$ docker run --rm -it -p 50000:50000 -p 8080:8080 -v /Users/HOST_PATH/OpenWhisk-weather-report-serverless/jenkins:/var/jenkins_home OpenWhisk-weather-report-serverless:latest
  1. 现在,我们将打开浏览器,导航到 Jenkins 应用程序并创建一个作业。由于我的 Docker 容器在本地主机上运行,所以我的 URL 是 http://localhost:8080/

  2. 打开 Chrome/IE 浏览器,粘贴上述本地 URL 并打开 Jenkins。在首页,系统会要求输入管理员密码。我们可以从容器运行日志中获取这个密码,格式应类似如下:

Jenkins initial setup is required. An admin user has been created and a password generated.

Please use the following password to proceed to installation: 7d4a4a91efb24f369bac95122686bd45
  1. 一旦我们输入密码,系统将提示我们安装建议的插件。继续进行此操作。它是一次性的活动,因为我们将在 Docker 主机上保存插件,插件添加完成后,系统会要求我们创建一个管理员用户。创建用户并确保记住密码。然后,一旦用户创建完成,就可以开始使用 Jenkins。

  2. 现在,让我们创建一个名为 Serverless-OpenWhisk 的作业。点击“新建项目”,这会打开一个新页面。在文本框中输入 Serverless-OpenWhisk,选择“自由风格项目”,然后点击 OK。

  3. 现在,在源管理选项卡中选择 Git,并在“仓库 URL”中添加 https://github.com/shzshi/OpenWhisk-weather-report-serverless.git。由于这是一个公共仓库,我们不需要输入凭据。如果使用私人 GitHub 仓库,则需要为 Jenkins 提供凭据来克隆仓库。

  4. 接下来,在构建选项卡中的“添加构建步骤”下拉菜单中,选择“执行 shell”。会弹出一个命令文本框。让我们在执行 shell 字段中添加以下代码。您需要将高亮的 OW_AUTHOW_APIHOST 键替换为我们之前创建的密钥,或者我们可以登录到 OpenWhisk 门户并获取相关信息:

npm config set prefix '~/.npm-global'
export PATH=~/.npm-global/bin:$PATH
npm install
./node_modules/mocha/bin/mocha test/test.js
export OW_AUTH=00700a7f-2b1a-4831-bf32-3a566263ed44:4TBcM7f8g0gj5UPgSVHXwNmMkfpbX36OdWximngOwqZYAJrDSkZwPjeSPjQ45Wm1
export OW_APIHOST=OpenWhisk.eu-gb.bluemix.net
serverless deploy -v
serverless invoke --function main -d '{"location":"Paris"}'
  1. 完成后,保存工作,然后点击立即构建链接。工作将开始运行。它将下载并安装package.json中提到的 npm 依赖项,然后运行 mocha 单元测试,将操作部署到 OpenWhisk 云中,然后用一个参数调用该函数/操作,以确保部署成功。输出将类似如下:
Building in workspace /var/jenkins_home/workspace/Serverless-OpenWhisk
Cloning the remote Git repository
Cloning repository https://github.com/shzshi/OpenWhisk-weather-report-serverless.git
 > git init /var/jenkins_home/workspace/Serverless-OpenWhisk # timeout=10
Fetching upstream changes from https://github.com/shzshi/OpenWhisk-weather-report-serverless.git
 > git --version # timeout=10
 > git fetch --tags --progress https://github.com/shzshi/OpenWhisk-weather-report-serverless.git +refs/heads/*:refs/remotes/origin/*
 > git config remote.origin.url https://github.com/shzshi/OpenWhisk-weather-report-serverless.git # timeout=10
 > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git config remote.origin.url https://github.com/shzshi/OpenWhisk-weather-report-serverless.git # timeout=10
Fetching upstream changes from https://github.com/shzshi/OpenWhisk-weather-report-serverless.git
 > git fetch --tags --progress https://github.com/shzshi/OpenWhisk-weather-report-serverless.git +refs/heads/*:refs/remotes/origin/*
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
 > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
Checking out Revision af9511727c7610915701372a7d617e73a03825f5 (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f af9511727c7610915701372a7d617e73a03825f5
Commit message: "updated jenkin host"
First time build. Skipping changelog.
[Serverless-OpenWhisk] $ /bin/sh -xe /tmp/jenkins660311873862513550.sh
+ npm config set prefix ~/.npm-global
+ export PATH=~/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ npm install
---
---
Finished: SUCCESS

本次练习的目的是展示如何通过 Jenkins、Mocha 和 Serverless Framework 设置持续集成。在接下来的章节中,我们将学习如何使其更加健壮,并了解如何使用相同的工具集设置持续交付管道。我们还将使用 gate 设置不同环境间的自动化部署。

OpenWhisk 的持续交付管道

持续交付是 DevOps 的核心功能之一。它不仅有助于加速生产环境的部署,还能帮助创建无 BUG 的应用程序。我们将使用 Jenkins、Serverless Framework 和 OpenWhisk 云的功能,为 OpenWhisk 函数构建一个持续交付平台。我们将继续使用之前构建的天气系统应用,并增强其以符合持续交付模型。我已经将所需的文件添加到本章前面提到的 Git 仓库中。通过在根目录中使用 Jenkinsfile,我们为该项目定义了一个多阶段管道。Jenkinsfile 是 Jenkins 用来运行管道的 groovy 脚本。我们将使用 master 分支进行部署和测试,覆盖从开发阶段到生产的各个环境,前提是部署和测试在管道中都通过。每次部署后(生产环境除外)都会运行集成测试,验证已部署的堆栈是否按预期工作。我们创建的这个管道将使用 Jenkinsfile 和 Serverless Framework,创建多个 Cloud Foundry 空间用于不同的环境,进行单元测试并构建版本推广。让我们通过以下步骤从 Cloud Foundry 空间开始:

  1. 登录 Bluemix at https://bluemix.net

  2. 导航到 Manage | Account | Cloud Foundry Orgs。

  3. 点击查看详情链接,然后点击链接添加 Cloud Foundry 空间。应弹出一个窗口。

  4. 添加四个环境:开发环境、SIT、预生产和生产。

创建空间后,我们将为该环境创建 API 密钥,并将其作为参数推送到 Jenkins 中,以便用于部署到不同的环境:

  1. 点击 Bluemix 门户左侧的三条线。

  2. 然后进入 Functions 并点击它,然后点击 API Key 链接。

  3. 我们将复制所有环境的主机和密钥。我们可以通过 CLOUD FOUNDRY SPACE 下拉框更改并切换到不同环境,如下图所示:

现在我们将转到 Jenkins,在那里我们将创建一个流水线作业。我已经将 Jenkinsfile 添加到 Git 仓库中,地址为 github.com/shzshi/OpenWhisk-weather-report-serverless/blob/master/Jenkinsfile,我们将在这里使用相同的 Git 来设置流水线。

我们将使用之前在教程中创建的相同 Jenkins Docker 容器。我们将创建一个新作业并进行操作。请按照以下步骤进行操作:

  1. 在浏览器中打开 Jenkins,地址为 http://localhost:8080

  2. 登录 Jenkins 并创建一个新项目。

  3. 输入项目名称为 OpenWhisk_serverless_pipeline,选择“流水线”(Pipeline),然后点击确定(OK)。

  4. 我们将被重定向到另一个页面,在那里我们需要用 OpenWhisk auth、OpenWhisk host 和流水线的 Git 路径对流水线进行参数化。为此,我们将勾选“此项目已参数化”(This project is parameterised)复选框。

  5. 然后,在“添加参数”(Add Parameter)下拉菜单中,我们需要选择“凭证参数”(Credentials Parameter)作为流水线的参数。下图显示了在凭证参数中添加详细信息的页面。我们必须为之前在 Azure 门户中创建的所有环境添加凭证参数。本凭证类型应为“秘密文本”,因为这将有助于我们隐藏身份验证密钥,这些密钥作为环境变量传递到 Jenkins 流水线中。因此,总共我们将创建五个凭证参数:四个用于开发、SIT、预生产和生产环境,另一个用于 OpenWhisk 主机。秘密文本值应从 Bluemix 门户中的 API 密钥部分填写。默认值应通过点击“添加”按钮,然后选择 Jenkins 来填充,这将帮助我们将 auth 密钥添加到 Jenkins 凭证提供者中。以下截图提供了更多信息:

  1. 我们将凭证设置为“秘密文本”(Secret text)。然后,秘密文本框将填充每个环境的 API 密钥,我们可以从 Azure 门户获取这些密钥。接着,ID 和描述字段将分别给出一个简单的名称,如 OpenWhisk_DEV_AUTH,作为参考,如下图所示:

  1. 当所有环境的参数添加完成后,我们需要转到“流水线”(Pipeline)选项卡并添加 GitHub 路径。下图显示了我们如何为 Jenkins 流水线添加配置:

  1. 一切设置完成后,我们需要用参数运行作业。一旦作业成功运行,如下图所示,Serverless Framework 会打包函数文件,并将它们部署到我们正在通过 serverless invoke 测试的不同环境中:

OpenWhisk 的部署模式

有多种部署模式可用,但在 DevOps 世界中,有两种模式非常流行。一种是金丝雀部署,另一种是蓝绿部署。我尽力将无服务器部署适配到这些模式中,但仍有改进的空间。让我们来看看如何将 OpenWhisk 动作适配到这些模式中。

金丝雀部署

金丝雀部署模式涉及将新版本逐步推出到部分用户或服务器。然后我们对其进行测试,如果表现良好,我们就将其推广到剩余的用户或服务器。这样做的优势在于失败的概率非常低,我们可以在不影响所有流量的情况下分析并解决问题。因此,对于 OpenWhisk,我们可以通过使用区域并将一定比例的流量路由到该区域中的新发布版本来进行设置。如果您不想创建该区域或组织,那么您可以在 Bluemix 门户上将 Cloud Foundry 空间设置为灾难恢复DR)环境,并将一定比例的流量转移到 DR 中。这里最好的部分是,如果您发现发布版本有问题,那么您应该能够回滚到先前的版本。

目前,通过 CLI 或 Serverless 框架并没有设置流量百分比路由到特定动作的功能。因此,我们必须通过动作的代码手动设置这一点,就像在 Lambda 中所做的那样。但一旦 OpenWhisk 进一步发展,我们应该会看到这个功能被添加。

蓝绿部署

尽管蓝绿部署与金丝雀部署相似,但两者的区别在于,蓝绿部署是使用两个独立且相同的环境并行运行,以减少引入新版本动作的风险,而不是简单地路由流量的百分比。为此,我们创建一个名为暂存的新环境。生产环境用于上线,而暂存环境用于处理新变化。然后,我们将在暂存环境和生产环境之间来回切换。

对于 OpenWhisk,我们可以通过使用 Cloud Foundry 空间创建一个新的暂存环境来实现这一点。然后,我们将当前发布版本的动作部署到蓝色(生产)环境,并通过别名将流量重定向到蓝色环境。接着,在新版本的动作开发完成后,我们将其部署到绿色(暂存)环境进行测试。一旦测试结果令人满意,我们将所有流量重定向到绿色(暂存)环境,并将其投入生产。随后,新版本将被部署到蓝色版本进行测试。

如果性能下降或发布版本有问题,我们会将蓝色(上线)版本回滚到当前版本,并将流量重新引导到蓝色环境,绿色环境则变为暂存环境。

动态仪表板

IBM Cloud 提供了一个开箱即用的监控仪表板,它提供了函数调用的图形概述。它还帮助确定云函数操作的性能和健康状况。监控页面分为三个选项卡——活动摘要、活动日志和活动时间线。让我们详细了解每个选项卡。

活动摘要

Activity Summary 小部件提供了云函数环境的高级概述。它帮助我们理解并监控启用云函数服务的整体健康和性能。该指标提供以下详细信息:

  • OpenWhisk 操作的使用率,显示它们被调用的次数。

  • 所有操作的总体失败率。我们可以通过活动日志小部件发现错误并隔离出发生错误的服务。

  • 操作的性能,显示附加到每个操作的平均完成时间。

活动时间线

Activity Timeline 小部件显示一个垂直条形图,展示操作的活动。红色表示特定操作中的错误,我们可以将此视图与 Activity Log 关联,以理解操作中的具体错误。

Activity Log

Activity Log 小部件以我们可以使用的格式显示激活日志,帮助我们查看每次激活的详细信息,如下所示的截图。如果点击 按钮查看详细日志,它还会显示每次调用所花费的时间以及日期:

我们还可以像上一章那样设置 Hygiea,监控 OpenWhisk 操作的构建、开发和测试。我们还可以使用 Hygiea 设置部署仪表板,以便跟踪不同环境中的部署。正如前面提到的,Hygiea 是监控 DevOps 进展和性能的理想仪表板。

OpenWhisk 操作日志管理

日志记录 在无服务器应用中非常有限,因为没有虚拟机、操作系统和中间件包。但我们必须充分利用我们所拥有的日志功能,以便有效地调试和解决 OpenWhisk 操作中可能遇到的问题。值得庆幸的是,我们仍然可以访问操作的输出日志。所以我们要做的唯一事情就是将文件推送到 Elasticsearch,并为更好的使用进行索引。在本节中,我们将了解如何做到这一点。

我们将在本地使用 Docker 设置 ELK,并将 OpenWhisk 操作日志推送到 ELK。OpenWhisk 云服务自带 ELK 配置,因此我们可以在门户网站上查看聚合的日志。为了实现这一点,我们需要登录到 Bluemix 门户。请按照以下步骤操作:

  1. 登录 Bluemix 门户网站 console.bluemix.net/

  2. 点击 Functions 链接,这将带你到函数/操作门户。

  3. 在门户中,点击日志。我们将进入一个包含 Kibana 仪表盘的页面。在那里,我们可以搜索所需的日志。关于如何查看这些日志的更多细节可以参考console.bluemix.net/docs/openwhisk/openwhisk_logs.html#viewing-activation-logs-in-the-ibm-cloud

然而,在门户上维护日志是非常昂贵的,且长时间保存日志更加困难且成本高昂,因此为了节省基础设施成本,我们可以将日志拉取到本地并长时间保留,且成本要低得多。接下来,我们将看看如何在本地设置 ELK 并拉取日志。

设置 ELK

我们将使用 Docker 在本地设置 ELK。我们在许多章节中使用过 Docker,因此使用 Docker 设置 ELK 对我们来说应该很容易。我们将使用 Docker Hub 上的官方 Docker 镜像进行设置。我们将使用 Elasticsearch 2.4.1、Logstash 2.4.0 和 Kibana 4.6.1。请输入以下命令:

$ docker run -p 5601:5601 -p 5000:5000 -it --name elk sebp/elk:es241_l240_k461

当你运行前面的命令时,Docker 将连接到 Docker Hub,下载镜像,然后为 ELK 堆栈创建容器。成功加载 Docker 容器后,我们应该能够通过浏览器使用localhost:5601访问 Kibana 仪表盘。

现在我们需要从本地主机端口5000创建一个公共主机名和端口,因此我们需要隧道化我们的本地主机。对于我们的示例,我们将使用burrow.io

OpenWhisk 动作

我们将使用本章前面创建的现有 OpenWhisk 动作。现在,让我们登录到 Bluemix 门户。进入 Functions | Actions | On monitor。我们应该能够在活动日志小部件中看到 OpenWhisk 函数/动作的日志。我们需要将这些日志拉取到本地 ELK。我们将使用天气报告应用的日志作为示例。

OpenWhisk 日志转发器

J Thomas 编写了一个 OpenWhisk 转发器,它是另一个 OpenWhisk 函数。这个函数将把 OpenWhisk 动作日志推送到本地 ELK 堆栈。让我们来看看这个是如何完成的。首先,我们需要克隆他创建的公共代码库,做一些修改,然后将这个函数部署到 Bluemix 门户的特定空间。这个函数将使用 burrow 隧道化的主机和端口将日志推送到本地 ELK:

  1. 使用以下代码克隆 j thomas 的代码库:
$ git clone https://github.com/jthomas/OpenWhisk-logstash-forwarder.git
  1. 导出 OpenWhisk authapi 主机,用于部署到 Bluemix 门户。你需要登录门户以获取最新的详情,使用以下代码:
$ export OW_AUTH=00700a7f-2b1a-4831-bf32-3a566263ed44:4TBcM7f8g0gj5UPgSVHXwNmMkfpbX36OdWximngOwqZYAJrDSkZwPjeSPjQ45Wm1
$ export OW_APIHOST=OpenWhisk.eu-gb.bluemix.net 
  1. 安装 serverless OpenWhisk 插件和最新版本的 Serverless Framework(如果尚未安装),使用以下代码:

$ npm install --global serverless serverless-openwhisk

/usr/local/bin/serverless -> /usr/local/lib/node_modules/serverless/bin/serverless
/usr/local/bin/slss -> /usr/local/lib/node_modules/serverless/bin/serverless
/usr/local/bin/sls -> /usr/local/lib/node_modules/serverless/bin/serverless
> serverless@1.26.1 postinstall /usr/local/lib/node_modules/serverless
> node ./scripts/postinstall.js
serverless@1.26.1
 + serverless-OpenWhisk@0.12.0
 added 5 packages, removed 2 packages and updated 38 packages in 13.232s
  1. 安装依赖项,如以下代码所示:
$ npm install
 WARN registry Unexpected warning for https://registry.npmjs.org/: Miscellaneous Warning EINTEGRITY: sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw== integrity checksum failed when using sha512: wanted sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw== but got sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=. (57090 bytes)
 --- ---
> fsevents@1.1.2 install /Users/shashi/Documents/packt/chapter5/ELK/OpenWhisk-logstash-forwarder/node_modules/fsevents
 > node install
[fsevents] Success: "/Users/shashi/Documents/packt/chapter5/ELK/OpenWhisk-logstash-forwarder/node_modules/fsevents/lib/binding/Release/node-v57-darwin-x64/fse.node" already installed
 Pass --update-binary to reinstall or --build-from-source to recompile
> OpenWhisk-logstash-forwarder@0.1.0 postinstall /Users/shashi/Documents/packt/chapter5/ELK/OpenWhisk-logstash-forwarder
 > npm link serverless-OpenWhisk
/Users/shashi/Documents/packt/chapter5/ELK/OpenWhisk-logstash-forwarder/node_modules/serverless-OpenWhisk -> /usr/local/lib/node_modules/serverless-OpenWhisk
 added 606 packages in 9.933s 

Update available 5.5.1 → 5.8.0
Run npm i -g npm to update
  1. 使用无服务器架构部署动作,如以下代码所示:
 $ serverless deploy
 Serverless: Packaging service...
 Serverless: Excluding development dependencies...
 Serverless: Compiling Functions...
 Serverless: Compiling Packages...
 Serverless: Compiling API Gateway definitions...
 Serverless: Compiling Rules...
 Serverless: Compiling Triggers & Feeds...
 Serverless: Deploying Functions...
 Serverless: Deploying Triggers...
 Serverless: Binding Feeds To Triggers...
 Serverless: Deploying Rules...
 Serverless: Deployment successful! Service Information
 platform: OpenWhisk.eu-gb.bluemix.net
 namespace: _
 service: logging
packages:
 no packages deployed
actions:
 logging-dev-logstash-forwarder myTEST weatherReport-dev-main
triggers:
 logging_logstash-forwarder_schedule_trigger
rules:
 logging_logstash-forwarder_schedule_rule
endpoints (api-gw):
 **failed to fetch routes**
 api gateway key is wrong or has expired! if it has expired, please refresh with wsk bluemix login
endpoints (web actions):
 no web actions deployed

一旦操作或功能成功部署,我们将看到与本地 ELK 堆栈相关的日志,并且应该能够查看它们。

总结

在本章中,我们了解了什么是 OpenWhisk,并探讨了简化构建、部署、监控和记录 OpenWhisk 功能的各种方法。我们还学习了如何设置持续交付,并创建了一个管道来实现这一点。

在下一章中,我们将学习 Google 函数,了解它们是什么,以及如何通过使用简单的函数,利用 Serverless 框架和其他开源工具设置自动化管道。

第六章:使用 Google Functions 的 DevOps

Google 决定将其无服务器平台推出为 Cloud Functions,但它仍处于成熟阶段。在写这本书时,我们只能使用 Node.js 编写 Google Cloud Functions。这些函数可以通过 Google 内部的事件总线——Pub/Sub 和通过 HTTP 作为来自 Firebase 的移动事件触发。我不会深入讨论 Google Functions 的功能,因为我们在前面的章节中已经讲解过了。因此,在本章中,我们将讨论如何将 DevOps 应用于 Google Functions,以及部署、发布管理、监控和日志记录的最佳实践。我们将通过 gcloud 命令行、Serverless Framework 和 Jenkins 进行各种示例和演示。好了,让我们进入 DevOps 主题。

使用 Google Functions 的 CI 和 CD 流水线

由于 Google 目前只允许使用 JavaScript 编写代码,因此我们将在本书中使用 Node.js,并提供示例和演示。Google 将其无服务器函数称为 Cloud Functions,因此我们将在本章中始终使用这个术语。因此,Cloud Functions 需要用 JavaScript 编写,并在 Node.js v6.11.5(在写这本书时)中执行,且云函数的源代码必须以 Node.js 模块形式导出。该模块将通过 require() 调用加载。因此,函数都包含在一个 index.js 文件中。我们可以通过 HTTP 请求方法(如 GETPOSTPUTOPTIONSDELETE)来调用该函数。部署可以通过 Google Cloud CLI 提供的命令行工具、GCP 控制台上的云函数 UI 或通过无服务器框架来完成。在本章中,我们将探讨每种方式。可部署的文件是一个 ZIP 文件,其中包含打包的函数,并部署到 Google Cloud 存储桶中。函数的源代码也可以直接放入 Cloud Functions,或者通过上传到 Google Cloud 存储桶来引用它。让我们看看如何通过 Jenkins 流水线进一步自动化函数的部署。

云函数的前提条件

让我们来看一下如何使用 GCP 控制台创建和部署云函数。但在此之前,我们需要先访问 Google Cloud Functions 平台。在写这本书时,Google 提供了一个免费的 GCP 账户,内含 300 美元的信用额度,有效期为一年,可用于 GCP 上的任何产品。接下来让我们创建一个。请按照以下步骤操作:

  1. 访问以下链接:cloud.google.com/free/

  2. 点击“免费试用”按钮,页面将重定向到 Google 账户页面。如果您已经有 Google 账户,可以直接使用它,或者创建一个新的 Google 账户。

  3. 输入凭据后,您将被重定向到 GCP 首页。

  4. 由于帐户已创建,我们将创建一个 GCP 项目。请前往“管理资源页面”(console.cloud.google.com/cloud-resource-manager)。

  5. 我们现在将创建一个项目,点击“创建项目”,输入项目名称为 My Serverless Project 并点击“创建”。您将在页面右上角的通知铃铛图标中看到项目正在创建。创建完成后,刷新页面,您将看到该项目出现在列表中。

  6. 现在通过打开链接启用项目的云 API:console.cloud.google.com/flows/enableapi?apiid=cloudfunctions&redirect=https://cloud.google.com/functions/quickstart。页面会显示一个下拉列表,供您选择项目。选择“我的无服务器项目”并点击“继续”,然后该 API 将为项目启用。

  7. 我们现在将安装并配置 Google Cloud SDK。以下链接将引导您完成此过程:cloud.google.com/sdk/docs/。在执行 gcloud init 时,您将被提示选择项目。选择我们创建的项目“我的无服务器项目”,并且我们也将完成 GCP 认证。

  8. 设置 Node.js 环境。

通过 GCP 控制台使用云函数

我们现在将通过 GCP 控制台创建云函数:

  1. 前往 GCP 控制台中的云函数概览页面,从下拉列表中选择关联的项目。

  2. 点击创建函数。

  3. 将函数命名为 my-serverless-function

  4. 选择触发器为 HTTP 触发器。

  5. 我们使用默认代码,因此它应该由内联编辑器检查。文本区域中会有两个脚本,一个是 index.js,另一个是 package.json。这是 Google Functions 提供的一个简单的 helloworld 函数。该函数会记录您在后续步骤中提供的消息。完成步骤后,您将看到类似下面的截图:

  1. 点击创建按钮。现在,功能将被部署,GCP 控制台将重定向到“概览”页面。在功能部署过程中,旁边的图标是一个小的旋转图标。部署完成后,旋转图标将变为绿色勾号。

  2. 我们将测试该功能。点击页面右侧特定功能旁边的三个竖点,然后点击“测试功能”:

  1. 我们将被重定向到一个函数测试页面。在触发事件字段中,将文本替换为 {"message":"Hello World!"} 并点击“测试函数”。在输出字段中,我们应该看到 Success: Hello World!,在日志字段中,状态码 200 表示函数运行成功。我们可以通过点击每个日志的箭头查看详细日志。

  2. 我们可以通过点击右上角的“查看日志”来查看日志历史。因此,这就是我们通过 GCP 控制台创建和部署函数,并查看函数日志及其历史的方式:

使用 gcloud 命令行部署云函数

本章的这一部分,我们将通过 gcloud 命令行部署 helloworld 应用程序。首先,我们需要通过“管理资源”页面创建一个 GCP 项目,启用云函数 API 作为前提条件,最重要的是,已经在本地安装了 gcloud SDK:

  1. 让我们更新 gcloud 组件。打开命令提示符,输入以下命令。你将被提示使用 gcloud beta 命令安装组件。安装后,gcloud 选中的组件将被安装并在本地配置:
$ gcloud components update && gcloud components install beta

我们也可以使用 Google Cloud Shell 来跟随部署步骤(console.cloud.google.com/?cloudshell=true)。Google Cloud Shell 是一个命令行环境,GCP 控制台中预装了 gcloud SDK。

  1. 让我们为此创建一个函数。我们需要创建一个目录,以便我们的函数放置在其中:
$ mkdir helloServerless
$ cd helloServerless
  1. 让我们在 helloServerless 目录下创建一个文件 index.js,并填入以下内容:
/**
* HTTP Cloud Function.
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.helloServerless = (req, res) => {
  res.send('Hello My Serverless World!');
};
  1. 我们将使用以下命令部署函数。该命令会将函数部署到 GCP 中。--trigger-http 是我们需要指定的触发器。通过此触发器部署时,函数将分配一个端点,可以使用 describe 命令查看该端点。部署时可以使用多种不同的触发器:
$ gcloud beta functions deploy helloServerless --trigger-http

Deploying function (may take a while - up to 2 minutes)...done.
 availableMemoryMb: 256
 entryPoint: helloServerless
 httpsTrigger:
 url: https://[GCP_REGION]-[PROJECT_ID].cloudfunctions.net/helloServerless
 labels:
 deployment-tool: cli-gcloud
 name: projects/my-serverless-project-201920/locations/us-central1/functions/helloServerless
 serviceAccountEmail: my-serverless-project-201920@appspot.gserviceaccount.com
 sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-39234144-2d9a-4a19-bf5f-82ef8d19a71b/d2611724-50d0-4339-9ee5-9d140aa674ae.zip?GoogleAccessId=37314512027@cloudservices.gserviceaccount.com&Expires=1524947393&Signature=WCwiJ8E%2B8a6EMMZiz5cWG%2FE%2F7pt8V%2FbqIDyW68utF%2FQXMJR0Pt94e0fC8DcqHbWBAEqHaE9%2B71fOYoWVcXg%2FWos38q%2FIfl%2BTNfaavRr9WyIi6V3M5our8aNI%2FGcntOrHm%2FpMK2LdiBzN6QI%2FiDjPDaA5%2FXohjLD%2BLaGGDHEoHp97%2BqCA7Mzifs%2B%2BhNcYknZ0vjLdpegUqfTUVjDF1h%2BQqg68sIurtT14Bay3uXryeMqYB%2FZv0lTjZIsd7svnUZtXZTNi72HOzP2J7vo1qo%2B74hO7Yy7WJOj2yvDKiFJ4ZXyk8cXvPu3ooNV%2BGy2dc758CFl3u87M%2FuKJV%2Bl0VkDwnQ%3D%3D
 status: ACTIVE
 timeout: 60s
 updateTime: '2018-04-28T19:59:54Z'
 versionId: '1'
  1. 一旦函数成功部署,我们可以通过部署输出中的 URL 属性执行该函数,或者通过以下命令获取 URL:
$ gcloud beta functions describe helloServerless
  1. 复制、粘贴并运行 URL,在任何浏览器中打开后,我们应该能够看到以下信息:
Hello My Serverless World!

如此,你可以看到通过 gcloud 在 GCP 上部署函数是多么简单。但我们如何确保对其进行单元测试、集成测试,最终实现自动化部署呢?这就是我们将在本章后续部分讨论的内容。不过,大部分工作我们可以通过设置本地开发环境来完成。

本地构建和测试

由于云函数运行在 Node.js 环境中,我们可以仅使用带有开发工具的 Node.js 模拟器在本地构建和测试应用程序。该模拟器是开源的,代码托管在 GitHub 上。让我们看看如何使用它:

  1. 通过 npm 或 Yarn 安装模拟器;你需要确保机器上已安装 Node.js
$ npm install -g @google-cloud/functions-emulator

/usr/local/bin/functions-emulator -> /usr/local/lib/node_modules/@google-cloud/functions-emulator/bin/functions
 /usr/local/bin/functions -> /usr/local/lib/node_modules/@google-cloud/functions-emulator/bin/functions

> @google-cloud/functions-emulator@1.0.0-beta.4 postinstall /usr/local/lib/node_modules/@google-cloud/functions-emulator
 > node scripts/upgrade-warning

 If you're using the Emulator via the Firebase CLI, you can
 disregard this message.

If you're upgrading @google-cloud/functions-emulator, these
 are the recommended upgrade steps:
1\. Stop the currently running emulator, if any:
functions stop
2\. Uninstall the current emulator, if any:
npm uninstall -g @google-cloud/functions-emulator
3\. Install the new version of the emulator:
npm install -g @google-cloud/functions-emulator

If you have trouble after upgrading, try deleting the config
 directory found in:

~/.config/configstore/@google-cloud/functions-emulator

Then restart the emulator. You can also check for any renegade
 Node.js emulator processes that may need to be killed:

ps aux | grep node
+ @google-cloud/functions-emulator@1.0.0-beta.4
 added 314 packages in 21.252s
  1. 让我们启动模拟器。此命令将启动模拟器并等待提示:
$ functions start
 Warning: You're using Node.js v8.9.2 but Google Cloud Functions only supports v6.11.5.
 Starting Google Cloud Functions Emulator...
 Google Cloud Functions Emulator STARTED
 No functions deployed "_". Run functions deploy --help for how to deploy a function.
  1. 在本地部署函数;在函数文件夹之前进入一个文件夹并运行以下命令:
$ functions deploy helloServerless --trigger-http
 Warning: You're using Node.js v8.9.2 but Google Cloud Functions only supports v6.11.5.
 Copying file:///var/folders/mj/74rcnfp94ll_sj3s95z4nnx00000gn/T/tmp-6681yxqJsPl2WsYI.zip...
 Waiting for operation to finish...done.
 Deploying function......done.
 Function helloServerless deployed.
 ┌────────────┬──────────────────────────────────────────────────────────────────────────────────┐
 │ Property │ Value │
 ├────────────┼──────────────────────────────────────────────────────────────────────────────────┤
 │ Name │ helloServerless │
 ├────────────┼──────────────────────────────────────────────────────────────────────────────────┤
 │ Trigger │ HTTP │
 ├────────────┼──────────────────────────────────────────────────────────────────────────────────┤
 │ Resource │ http://localhost:8010/my-serverless-project-201920/us-central1/helloServerless │
 ├────────────┼──────────────────────────────────────────────────────────────────────────────────┤
 │ Timeout │ 60 seconds │
 ├────────────┼──────────────────────────────────────────────────────────────────────────────────┤
 │ Local path │ /Users/<username>/Documents/packt/chapter6/googlefunction_hello_world │
 ├────────────┼──────────────────────────────────────────────────────────────────────────────────┤
 │ Archive │ file:///var/folders/mj/74rcnfp94ll_sj3s95z4nnx00000gn/T/tmp-6681yxqJsPl2WsYI.zip │
 └────────────┴──────────────────────────────────────────────────────────────────────────────────┘
  1. 打开输出中提到的 URL 在浏览器中查看。你应该看到消息 Hello My Serverless World!;或者通过命令行执行:
$ functions call helloServerless
 Warning: You're using Node.js v8.9.2 but Google Cloud Functions only supports v6.11.5.
 ExecutionId: 4b32f8b9-2452-45ee-a0c9-57f2fd3f9d8a
 Result: Hello My Serverless World!
  1. 此外,你还可以查看以下日志:
 $ functions call helloServerless
 Warning: You're using Node.js v8.9.2 but Google Cloud Functions only supports v6.11.5.
 ExecutionId: 4b32f8b9-2452-45ee-a0c9-57f2fd3f9d8a
 Result: Hello My Serverless World!
 <username>s-MacBook-Pro:googlefunction_hello_world <username>$ functions logs read
 Warning: You're using Node.js v8.9.2 but Google Cloud Functions only supports v6.11.5.
 2018-04-28T20:54:55.907Z - info: User function triggered, starting execution
 2018-04-28T20:54:55.914Z - info: Execution took 11 ms, user function completed successfully
 2018-04-28T20:56:10.393Z - info: User function triggered, starting execution
 2018-04-28T20:56:10.394Z - info: Execution took 1 ms, user function completed successfully
 2018-04-28T21:07:14.853Z - info: User function triggered, starting execution
 2018-04-28T21:07:14.859Z - info: Execution took 6 ms, user function completed successfully
 2018-04-28T21:12:13.228Z - info: User function triggered, starting execution
 2018-04-28T21:12:13.229Z - info: Execution took 0 ms, user function completed successfully

有关模拟器的更多细节可以在以下链接中找到。我们可以将模拟器与我们最喜欢的开发工具集成,并调试 Google 函数:github.com/GoogleCloudPlatform/cloud-functions-emulator。关于调试 Cloud Functions:github.com/GoogleCloudPlatform/cloud-functions-emulator/wiki/Debugging-functions

持续集成和持续部署(CI/CD)与测试

到目前为止,我们只是创建了一个函数,并提出了几种构建和部署的方法。我们还学会了如何通过模拟器在本地运行函数。但是,我们不能每次手动构建和部署每个函数,而且我们还应该对代码进行版本控制。在本部分的章节中,我们将深入探讨这些方面。

源代码管理

源代码管理是软件开发中非常重要的一部分。最佳做法是对代码进行版本控制和标签管理。Git 是最流行的源代码管理工具,我们在本章中一直在使用它。因此,为了实现完美的部署周期,我们应该始终创建不同的分支。包括功能分支、开发分支、发布分支,以及默认的主分支。我在之前的章节中已经讲解了有关最佳实践的内容,因此在这里不再讨论分支结构以及代码在分支间的流动管理。然而,关于 Cloud Functions 的文件夹结构是非常重要的。在之前的章节中,我们将函数写在了 index.js 中。如果函数数量较少,完全可以将它们管理在 index.js 文件中。但如果函数数量达到几百个,那么将它们集中在一个文件中管理就会变得非常繁琐和痛苦。因此,组织函数的一种简单方式如下所示:

├── /build/                # Compiled output for Node.js 6.x
├── /src/                    # Application source files
│   ├── someFuncA.js         # Function A
│   ├── someFuncA.test.js    # Function A unit tests
│   ├── someFuncB.js         # Function B
│   ├── someFuncB.test.js    # Function B unit tests
├── index.js                 # Main export(s)
└── package.json             # List of project dependencies and NPM scripts

结构化的方式有很多种,但我保持了简单。然而,我会让开发者自行决定结构。

持续集成与测试

持续集成和测试是开发周期中另一个重要方面。持续集成将代码合并在一起,测试确保进入生产环境的代码没有错误,并且大多数问题在低环境中得到解决。测试有许多不同的方式:单元测试、集成测试和系统测试。我们将在示例中将这些测试集成到自动化流水线中。我将使用谷歌云提供的示例和一个简单的hello world函数,自动化运行它们,然后将它们放入流水线进行一键部署。

我已经创建了一个HelloWorld函数,参考了谷歌现有的示例代码库。我已将其放入我的本地代码库中,我们将使用这个库来设置持续集成和测试。我还创建了一个 Dockerfile,它将帮助我们创建一个用于 Jenkins 的 Docker 容器,容器中预安装了gcloud、Node.js 和我们将在设置 DevOps 自动化时使用的函数模拟器:

  1. 克隆以下提到的 Git 代码库。我们将其克隆到本地,以获取 Dockerfile,你可以通过修改脚本并测试部署进行尝试:
$ git clone https://github.com/shzshi/google-functions-helloworld.git
  1. 我们将在本地构建一个 Docker 镜像,然后启动 Jenkins 门户,以便我们能够设置自动化。你需要确保在本地机器上安装了 Docker,以便运行此示例。

  2. 我们进入 Dockerfile 目录:

$ cd google-functions-helloworld
  1. 我们创建一个包含 Jenkins、gcloud、函数模拟器、Node.js 和所有其他所需库的 Docker 镜像:
$ docker image build -t google-functions .
  1. 在这里,运行前一行创建的 Docker 容器。Docker 容器将在本地托管,暴露808050000端口。我们还将与本地主机目录映射卷:
$ docker run --rm -it -p 50000:50000 -p 8080:8080 -v /My/Local/Host/PATH/chapter6/google-functions/jenkins:/var/jenkins_home google-functions:latest
  1. 一旦容器运行,我们将通过http://localhost:8080浏览 Jenkins 门户。如果这是第一次创建此容器且未映射卷,你将被要求复制并粘贴密码并安装默认插件。

  2. 使用之前创建的凭证登录 Jenkins 门户,或者使用已有的凭证。然后点击 New Item。

  3. 输入项目名称为my-serverless-google-functions,选择 freestyle 项目,然后点击 OK。

  4. 进入 Source Code Management 标签页,选择 Git,然后将下面提到的代码库 URL 复制并粘贴到 Repository URL 文本框中,其他保持默认:github.com/shzshi/google-functions-helloworld.git

  5. 我们需要在 Jenkins 中为gcloud创建一个 Google 服务账户,以便与 GCP 进行身份验证:

    • 访问 OPEN THE LIST OF CREDENTIALS 页面 (console.cloud.google.com/apis/credentials?_ga=2.77044693.-1734735492.1524930885)

    • 点击 Create Credentials

    • 选择 Service Account Key

    • 点击下拉菜单中的 Service account,并选择 New Service Account

    • 在 Name 中输入服务账户名称

    • 使用默认的服务账户 ID 或生成一个不同的 ID

    • 选择密钥类型:JSON

    • 点击创建,显示服务账户创建窗口,并且您选择的密钥类型的私钥会自动下载到本地计算机,我们将在后续使用它。

    • 点击关闭

  6. 您需要分叉您的代码库,并将 gcloud 服务账户密钥 JSON 文件的内容复制到文件 My-Serverless-Project-1d8bacd4886d.json 中,因为我们将在 Jenkins 中使用此 JSON 文件进行身份验证:github.com/shzshi/google-functions-helloworld.git

  7. 转到 Build 选项卡,在下拉菜单的 Add build step 中选择 Execute Shell,然后将以下步骤添加到 Command 文本区域:

gcloud auth activate-service-account --key-file=${WORKSPACE}/My-Serverless-Project-1d8bacd4886d.json
 gcloud config set project ${YOUR_GCP_PROJECT_ID}
 npm install

 export NODE_PATH=${WORKSPACE}/node_modules

# executing unit test
 ${WORKSPACE}/node_modules/.bin/ava test/unit.http.test.js

 # executing integration test
 export BASE_URL=http://localhost:8010/${YOUR_GCP_PROJECT_ID}/${YOUR_GCF_REGION}
${WORKSPACE}/node_modules/.bin/functions start
${WORKSPACE}/node_modules/.bin/functions deploy helloHttp --trigger-http
${WORKSPACE}/node_modules/.bin/ava test/integration.http.test.js
 # deploying to GCP project and executing system test
 gcloud beta functions deploy helloHttp --trigger-http
export BASE_URL=https://${YOUR_GCF_REGION}-${YOUR_GCP_PROJECT_ID}.cloudfunctions.net/helloHttp
${WORKSPACE}/node_modules/.bin/ava test/system.http.test.js
  1. 我们需要对作业进行参数化,这意味着我们需要添加两个文本参数,一个是 gcloud 项目 ID,另一个是 gcloud 区域。按照以下截图添加参数。默认值需要更改为您的项目名称和区域:

  1. 一切看起来正常后,按照前面 13 个步骤中的说明,点击 SAVE,保存项目。

  2. 要运行作业,请点击 Build with Parameters 并使用默认参数运行作业。

  3. 如果作业成功运行,我们应该看到以下输出,因此 Google Function 已成功通过单元测试、集成测试和系统测试:

Building in workspace /var/jenkins_home/workspace/my-serverless-google-functions
 > git rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository
 > git config remote.origin.url https://github.com/shzshi/google-functions-helloworld.git # timeout=10
Fetching upstream changes from https://github.com/shzshi/google-functions-helloworld.git
 > git --version # timeout=10
 > git fetch --tags --progress https://github.com/shzshi/google-functions-helloworld.git +refs/heads/*:refs/remotes/origin/*
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
 > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
Checking out Revision bbfb5c17a65dab7f0a8e0b3dc3de82e5792fe21d (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f bbfb5c17a65dab7f0a8e0b3dc3de82e5792fe21d
Commit message: "added function emulator"
 > git rev-list --no-walk bbfb5c17a65dab7f0a8e0b3dc3de82e5792fe21d # timeout=10
[my-serverless-google-functions] $ /bin/sh -xe /tmp/jenkins2470859504083131546.sh
+ gcloud auth activate-service-account --key-file=/var/jenkins_home/workspace/my-serverless-google-functions/My-Serverless-Project-1d8bacd4886d.json
Activated service account credentials for: [jenkins@my-serverless-project-201920.iam.gserviceaccount.com]
+ gcloud config set project my-serverless-project-201920
Updated property [core/project].
+ npm install
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.3 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

up to date in 30.041s
+ export NODE_PATH=/var/jenkins_home/workspace/my-serverless-google-functions/node_modules
+ /var/jenkins_home/workspace/my-serverless-google-functions/node_modules/.bin/ava test/unit.http.test.js

   helloHttp: should print a name
   helloHttp: should print hello world

  2 tests passed

+ export BASE_URL=http://localhost:8010/my-serverless-project-201920/us-central1
+ /var/jenkins_home/workspace/my-serverless-google-functions/node_modules/.bin/functions start
Warning: You're using Node.js v8.11.1 but Google Cloud Functions only supports v6.11.5.
Starting Google Cloud Functions Emulator...
Google Cloud Functions Emulator STARTED
┌────────┬───────────┬─────────┬──────────────────────────────────────────────────────────────────────────┐
│ Status │ Name      │ Trigger │ Resource                                                                 │
├────────┼───────────┼─────────┼──────────────────────────────────────────────────────────────────────────┤
│ READY  │ helloHttp │ HTTP    │ http://localhost:8010/my-serverless-project-201920/us-central1/helloHttp │
└────────┴───────────┴─────────┴──────────────────────────────────────────────────────────────────────────┘
+ /var/jenkins_home/workspace/my-serverless-google-functions/node_modules/.bin/functions deploy helloHttp --trigger-http
Warning: You're using Node.js v8.11.1 but Google Cloud Functions only supports v6.11.5.
Copying file:///tmp/tmp-1653qKGF3wXgKZdC.zip...
Waiting for operation to finish...done.
Deploying function............done.
Function helloHttp deployed.
┌────────────┬──────────────────────────────────────────────────────────────────────────┐
│ Property   │ Value                                                                    │
├────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Name       │ helloHttp                                                                │
├────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Trigger    │ HTTP                                                                     │
├────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Resource   │ http://localhost:8010/my-serverless-project-201920/us-central1/helloHttp │
├────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Timeout    │ 60 seconds                                                               │
├────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Local path │ /var/jenkins_home/workspace/my-serverless-google-functions               │
├────────────┼──────────────────────────────────────────────────────────────────────────┤
│ Archive    │ file:///tmp/tmp-1653qKGF3wXgKZdC.zip                                     │
└────────────┴──────────────────────────────────────────────────────────────────────────┘
+ /var/jenkins_home/workspace/my-serverless-google-functions/node_modules/.bin/ava test/integration.http.test.js

   helloHttp: should print a name (110ms)
   helloHttp: should print hello world (104ms)

  2 tests passed

+ gcloud beta functions deploy helloHttp --trigger-http
Deploying function (may take a while - up to 2 minutes)...
.............done.
availableMemoryMb: 256
entryPoint: helloHttp
httpsTrigger:
  url: https://us-central1-my-serverless-project-201920.cloudfunctions.net/helloHttp
labels:
  deployment-tool: cli-gcloud
name: projects/my-serverless-project-201920/locations/us-central1/functions/helloHttp
serviceAccountEmail: my-serverless-project-201920@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-39234144-2d9a-4a19-bf5f-82ef8d19a71b/5433a10a-52ed-4330-b68a-f3c3be016035.zip?GoogleAccessId=37314512027@cloudservices.gserviceaccount.com&Expires=1525049193&Signature=C3H2oyjrieTuykum%2BH09BqZO63bCXE4vlrWVzOhEMTBAoPAUFTHU96JVqTk7ZiGdl2iv34a7FlR70vE9oo4jnnZApVtCYHVSY9JA3X%2BAn4VR4Aw510UUC7ilZbGGJ5U3eyk1bVlzQxTkx20Mq6yx8JUQqRMj%2FTisHqs0MCHC9k83NJj6JQdF%2BbgVLzPg%2Bfrm06kZzhKqKmLQ7XOMXwSHlm%2F74N6%2B5JyByPvtPlHqgGoIdW8X7eyys4%2B22X3zU0Z3MjXG7emlA9t4Hrpa3oQ0AUiSD78b1Mnfbz%2FYXdj%2BKDY4fjv5JQcOBZLj8DEw8sbcSdJcIvvAKrPwcKy7Y1eiMg%3D%3D
status: ACTIVE
timeout: 60s
updateTime: '2018-04-30T00:16:35Z'
versionId: '2'
+ export BASE_URL=https://us-central1-my-serverless-project-201920.cloudfunctions.net/helloHttp
+ /var/jenkins_home/workspace/my-serverless-google-functions/node_modules/.bin/ava test/system.http.test.js

   helloHttp: should print hello world (371ms)
   helloHttp: should print a name (1.3s)

  2 tests passed

Finished: SUCCESS

所以,通过前面的示例,我们能够在本地和 gcloud 上构建、测试和部署函数。但我们只是随意地在一个项目上做了这一切。想象一下,如果我们需要有多个环境、性能系统和性能测试,并且尽可能减少手动干预,我们需要设置一个包含审批环节的管道,将函数部署到多个环境,在函数实际投入生产之前解决大部分问题。这就是持续交付发挥作用的地方。

使用 Google Functions 进行持续交付

持续交付是通过 减少风险、频繁交付并快速反馈,帮助加速产品上市的过程。那么,如何在 Google Cloud Functions 上做到这一点呢?有很多方法可以做到这一点,但我想使用无服务器框架来实现这一目标。这样做有几个原因。其中一个原因是,无服务器框架是一个非常成熟的无服务器功能部署框架。它支持许多不同的供应商,正如我们所看到的,并且有很好的社区支持。我们甚至可以通过 gcloud 和 Jenkins 管道来实现持续交付。我们将重用本章前面部分中使用的大部分设置。

Google 环境

为任何类型的应用程序设置持续交付时,拥有多个环境是至关重要的。但对于无服务器架构,环境的划分并不明确。因此,我们总是需要想出替代方案。我们对 Cloud Functions 也需要做同样的事情。我们通过两种方式设置环境分离——首先,我们可以为环境使用不同的名称来分隔功能,例如 my-serverless-devmy-serverless-sitmy-serverless-prod,但这会增加不必要的复杂性。因此,理想的方式是通过创建不同的项目来分隔环境,如下所示:

  1. 登录到 GCP 控制台,转到管理资源页面(console.cloud.google.com/cloud-resource-manager?_ga=2.108039562.-900655901.1524348645),点击创建项目,在项目名称文本框中输入 Serverless-SIT,然后点击创建。你会看到项目正在创建,页面右上角的通知铃铛图标会显示进度。创建完成后,刷新页面,你将看到项目出现在列表中。同样地,我们将创建 Serverless-UATServerless-PROD 项目。由于我们为 dev 环境使用的是本地模拟器,因此我不创建 dev 项目。

  2. 通过打开链接来为项目启用云 API:console.cloud.google.com/flows/enableapi?apiid=cloudfunctions&redirect=https://cloud.google.com/functions/quickstart。页面将显示一个项目下拉列表,选择My Serverless Project并点击继续,然后 API 将会为该项目启用。

  3. 登录到 Jenkins 门户 (http://localhost:8080),这是我们在本章的前面部分创建的。然后点击新建项目

  4. 在文本框中输入项目名称为 my-serverless-google-function-pipeline,然后从列表中选择管道,再点击确定

  5. 勾选此项目为参数化复选框,然后在添加参数下拉菜单中选择字符串参数。接着,在名称文本框中输入 DEV_PROJECT_ID,并在默认值文本框中输入我们在步骤 1 中创建的 dev 项目的 project id。同样,针对每个环境,我们可以创建一个文本参数,直到 PROD,最后一个文本参数应为 YOUR_GCF_REGION

  6. 点击管道选项卡,在定义下拉菜单中选择从 SCM 脚本管道,在 SCM 下拉菜单中选择Git。现在,在仓库 URL 文本框中输入 https://github.com/shzshi/google-functions-helloworld.git,然后保持其他设置为默认,点击保存

  7. 一旦任务保存完成,我们将构建流水线,为此我们需要点击Build with Parameters,这样我们可以看到每个参数的默认值。如果你想更改环境或添加不同的项目 ID,我们可以通过文本框输入。

  8. 现在点击“Build”,该任务应该首先初始化,即设置先决条件,然后它会运行单元测试,接着功能会被部署到本地开发环境中,之后进行集成测试。接下来的阶段中,功能会被部署到 UAT 环境中,其他环境也会进行同样的操作。生产环境的部署已经设置了有条件的审批。这个流水线可以通过多种方式完成。

监控与日志记录

Google 提供了一个仪表板,用于查看调用情况,并通过控制台查看调用日志。所以,一旦登录到控制台并选择特定的功能,我们应该能够看到调用图,并且能够查看执行时间和使用的内存。我们可以查看源代码并测试该功能。如果点击View Logs,如以下截图所示,我们应该能够看到调用日志,并且可以深入查看详细日志:

通过仪表板,Google Cloud 还提供了一个监控和日志记录平台,称为stackdriver。它为我们展示了 Cloud Function 的健康、性能和可用性的洞察。它与 Google Cloud 平台原生集成。Stackdriver 提供了各种各样的指标、仪表板、告警、日志管理、报告和跟踪功能。

它有先进的告警功能,帮助你快速识别问题。集成的日志记录、跟踪和错误报告能够快速深入分析并找出根本原因。

Stackdriver 让你可以访问日志、指标、跟踪和来自基础设施平台(如虚拟机、容器、中间件和应用层)的其他信号,帮助你从最终用户到后端服务和基础设施跟踪问题。对分布式系统、自动扩展和短暂资源的原生支持意味着你的监控能够无缝地与现代架构兼容。

最佳实践

Google Function 仍在 Beta 阶段,正在评估并日益改进。在 DevOps 的最佳实践方面,它与我在上一章节中建议的内容相似,我会推荐使用无服务器框架来自动化 Google Functions 中的部署。无服务器框架还提供了一个模板,用于为 Node.js 创建基本的功能设置。

在整个组织中发展 DevOps 文化并将单体应用程序转向微服务会产生巨大影响。但我们应该能够引导团队走上正确的道路。这需要一些指导,并通过新流程和术语来教育团队。

我们还必须将安全性扩展到 DevOps 工具和组织中。安全性应该是 DevOps 每个方面的一部分,从自动化测试、持续集成到持续部署过程,涵盖云平台中的所有内容。关注云中 DevOps 安全性监控是很有必要的,且应有专门的人负责此事。

供应商锁定在我们在 AWS 上开发功能或项目后,想要迁移到 Google Cloud 时,确实是很痛苦的。代码必须根据供应商要求进行修改。但在部署方面,我建议使用无服务器框架进行 DevOps,因为它能够缓解 DevOps 工具和框架的供应商锁定问题。

总结

在本章中,我们学习了如何使用 Google Functions 设置 CI 和 CD,并为 Google Functions 设置动态仪表盘。在下一章,我们将讨论如何设置我们自己的私有无服务器表单。我们还将讨论如何为这个无服务器架构设置 CI 和 CD,以及监控与日志记录、单元测试和集成测试。

第七章:为 Kubeless 添加 DevOps 风味

在上一章中,我们了解了 Google Functions 以及如何使用各种不同的工具和过程自动化部署 Google Functions。我们还研究了如何监控和记录服务。本章将介绍另一个开源无服务器框架——Kubeless。我们将学习如何在 minikube 上设置 Kubeless 框架,创建、部署和调用 Kubeless 函数,以及如何构建、部署、记录和监控 Kubeless 函数。

什么是 Kubeless?

Kubeless 是一个基于 Kubernetes 的开源无服务器框架。它允许我们部署和执行一段代码,而无需担心底层基础设施。它利用 Kubernetes 的资源提供自动扩展、路由和监控功能。部署后的函数可以通过发布–订阅、HTTP 和定时调度触发。发布–订阅事件通过 Kafka 集群进行管理,这是 Kubeless 内置的一个组件,包含一个基础的 Kafka 集群、一个代理和一个 zookeeper。HTTP 触发器通过 Kubernetes 服务提供,定时调度的函数则转化为 cron 作业。Kubeless 支持的语言/运行时包括 python2.7python3.4python3.6nodejs6nodejs8nodejs_distroless8ruby2.4php7.2go1.10dotnetcore2.0java1.8ballerina0.980.0jvm1.8。Kubeless 还支持 HTTP、NATS、Kafka、cron 和流式触发器。

Kubeless 架构

Kubeless 使用自定义资源定义,这意味着当你创建自定义资源定义时,Kubernetes API 服务器会为每个指定的版本创建一个资源路径。自定义资源定义可以是命名空间级别的或集群范围的,因此 CRD 被称为函数,这意味着 Kubeless 函数可以像普通的 Kubernetes 资源一样在后台创建,并且会创建一个控制器。控制器将监控这些自定义资源,并在运行时需求下启动。下图展示了该架构的工作原理:

如何设置 Kubeless

设置 Kubeless 非常简单。首先,我们从发布页面下载 Kubeless,创建一个命名空间,然后通过发布页面上的 YAML 清单,创建函数的自定义资源定义并启动控制器。如果你是在个人笔记本电脑上设置 Kubeless,那么我们需要使用 minikube 来实现这一点。接下来,让我们看看如何设置 minikube,因为我们将在本教程中使用它。

首先,通过访问 github.com/kubernetes/minikube 设置 minikube。安装完成后,我们应该能够在虚拟机内创建一个单节点的 Kubernetes 集群,并且能够通过命令提示符执行 minikube 命令。接下来,我们将创建一个集群并在其中创建 Kubeless 资源。然后,我们将创建一个简单的 Kubeless 函数并进行部署和调用。我们还将为 minikube 设置仪表盘,并查看控制器和函数是如何分别创建和部署的。让我们来看一下如何实现:

  1. 让我们创建一个 minikube 本地 Kubernetes 集群,如以下代码所示:
$ minikube start
Starting local Kubernetes v1.9.0 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.
Loading cached images from config file.
  1. 接下来,使用以下命令创建一个 minikube 仪表盘。该命令触发后会打开一个浏览器,并显示仪表盘。仪表盘将展示我们的服务、Pod 和管理器:
$ minikube dashboard
  1. 现在我们已经有了集群,让我们将 Kubeless 部署到集群中,如以下代码所示。针对多个 Kubernetes 环境(非 RBAC、RBAC 和 OpenShift)提供了多个 Kubeless 清单。我们将使用非 RBAC(非基于角色的访问控制)清单:
$ export RELEASE=$(curl -sk https://api.github.com/repos/kubeless/kubeless/releases/latest | grep tag_name | cut -d '"' -f 4)
$ kubectl create ns kubeless
$ echo $RELEASE
$ kubectl create -f https://github.com/kubeless/kubeless/releases/download/v1.0.0-alpha.8/kubeless-non-rbac-v1.0.0-alpha.8.yaml
serviceaccount "controller-acct" created
customresourcedefinition "functions.kubeless.io" created
customresourcedefinition "httptriggers.kubeless.io" created
customresourcedefinition "cronjobtriggers.kubeless.io" created
configmap "kubeless-config" created
deployment "kubeless-controller-manager" created
  1. 既然我们已经部署了 Kubeless,让我们检查它是否已正确部署:
$ kubectl get pods -n kubeless
NAME READY STATUS RESTARTS AGE
kubeless-controller-manager-c6b69df76-65gsh 1/1 Running 0 2m
$ kubectl get deployment -n kubeless
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
kubeless-controller-manager 1 1 1 1 3m
$ kubectl get customresourcedefinition
NAME AGE
cronjobtriggers.kubeless.io 3m
functions.kubeless.io 3m
httptriggers.kubeless.io 3m
  1. 接下来,我们需要在本地安装 Kubeless CLI,用于部署、调用和删除 Kubeless 函数,如以下代码所示:
$ export OS=$(uname -s| tr '[:upper:]' '[:lower:]') 
$ curl -OL https://github.com/kubeless/kubeless/releases/download/$RELEASE/kubeless_$OS-amd64.zip
$ unzip kubeless_$OS-amd64.zip 
$ sudo mv bundles/kubeless_$OS-amd64/kubeless /usr/local/bin/
  1. 让我们创建一个函数并进行部署。以下是一个简单的 Python 函数,我们将部署并调用它。创建一个名为 test.py 的文件,内容如下:
def hello(event, context):
 print event
 return event['data']

Kubeless 中的函数具有相同的格式,无论函数的语言或事件源如何。通常,每个函数将执行以下操作:

  • 接收一个对象 event 作为其第一个参数。该参数包含关于事件源的所有信息。特别是,data 键应包含函数请求的主体。

  • 接收第二个对象 context,它包含关于该功能的常规信息。

  • 返回一个字符串/对象,用作调用者的响应。

  1. 现在,让我们部署该函数,当我们刷新 minikube 仪表盘时,应该能够看到部署在其中的 hello 函数,如以下代码所示:
$ kubeless function deploy hello --runtime python2.7 --from-file test.py --handler test.hello --namespace kubeless
INFO[0000] Deploying function...
INFO[0000] Function hello submitted for deployment
INFO[0000] Check the deployment status executing 'kubeless function ls hello'

以下列表解释了前面代码中的各种元素:

  • kubeless function deploy hello 告诉 Kubeless 注册一个名为 hello 的新函数。通过此名称,函数将在网络上可访问。请注意,这个名称不需要与代码内部使用的函数名称相同(我们稍后会通过 --handler 选项指定)。

  • --trigger-http 告诉 Kubeless 该函数将通过 HTTP 被调用。也可以通过其他方式触发该函数,但此处未涵盖。

  • --runtime python2.7 告诉 Kubeless 使用 Python 2.7 来执行代码。节点也支持作为运行时,未来将支持更多的运行时。

  • --handler test.hello告诉 Kubeless 调用代码模块中的哪个函数。你可以在上面的 Python 代码中看到,函数名为hello

  • --from-file /tmp/hello.py告诉 Kubeless 上传并使用/tmp/hello.py文件作为函数的源文件。也可以通过其他方式传递函数。

我们将通过以下命令看到函数自定义资源的创建:

$ kubectl get functions
NAME AGE
hello 2m

$ kubeless function ls --namespace kubeless
NAME NAMESPACE HANDLER RUNTIME DEPENDENCIES STATUS
hello kubeless test.hello python2.7 1/1 READY
  1. 现在,让我们调用这个函数,如下所示:
$ kubeless function call hello --data 'Hello Serverless!' --namespace kubeless
Hello Serverless!
  1. 我们还可以delete该函数,如下所示:
$ kubeless function delete hello --namespace kubeless
$ kubeless function ls --namespace kubeless
NAME NAMESPACE HANDLER RUNTIME DEPENDENCIES STATUS

到目前为止,我们已经在本地安装了 Kubeless,创建了一个简单的函数,进行了部署、调用和撤销部署。接下来的部分,我们将学习如何使用 Serverless 框架自动化部署。

设置持续集成和部署

我们将使用 Serverless 框架来启动 Kubeless 函数的开发和部署。Serverless 框架提供了很多功能,可以轻松地采用 Kubeless。让我们来看看 Serverless 提供的各种功能。

创建服务

我们将使用 create 命令,通过传递运行时和 path 来创建基本服务并生成目录。目前,提供了两种运行时环境——Python 和 Node.js。所以,如果我们使用 path 参数运行以下命令,它将创建一个包含简单 Serverless 函数的文件夹。目前可用的运行时有 kubeless-pythonkubeless-nodejs

$ serverless create --template kubeless-python --path myKubelessFunc

create 命令将创建一个服务,并且每个服务的配置都将包含以下三个文件:

  • serverless.yml:此文件的主要职责是声明服务、定义提供商、定制插件(在我们的案例中是 serverless-kubeless 插件)、以及函数将执行的事件或触发器,并使用 serverless 变量配置文件。

  • handler.py:此文件将包含函数代码。serverless.yml 中的函数定义将指向 handler.py

  • package.json:这是我们函数的 npm 包定义文件,包含所有依赖项和 kubeless-serverless 插件。

让我们更新这些文件,包含我们的函数和配置,具体代码如下所示。我们正在更新一个函数,用于从通过 JSON 暴露的站点源中搜索自行车站。我还将代码上传到了 GitHub 仓库:github.com/shzshi/kubeless-serverless.git

#handler.py
import urllib2 
import json 
def find(event, context):     
    term = event['data']['term']     
    url = "https://feeds.capitalbikeshare.com/stations/stations.json"     response = urllib2.urlopen(url)     
    stations = json.loads(response.read())     
    hits = []     
    for station in stations["stationBeanList"]:         
        if station["stAddress1"].find(term) > -1:             hits.append(station)     

    return json.dumps(hits)

serverless.yaml 替换为以下内容:

# serverless.yml 
service: bikesearch 
provider:  
    name: kubeless  
    runtime: python2.7 
plugins:  
    - serverless-kubeless 
functions:  
    bikesearch:  
        handler: handler.find

部署函数

由于所需的文件是通过模板创建的,我们可以根据需要修改它们,然后在需要时简单地部署并调用它们。让我们使用 Serverless 部署它们,然后npm install将安装 Serverless 所需的依赖项,例如kubeless-serverless插件,接着我们可以部署函数,具体代码如下所示:

$ npm install
$ serverless deploy -v
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Deploying function bikesearch...
Serverless: Pods status: {"waiting":{"reason":"PodInitializing"}}
Serverless: Function bikesearch successfully deployed
Serverless: Skipping ingress rule generation

调用函数

Kubeless 函数可以通过命令行或 Kubeless 提供的 UI 调用。Kubeless UI 可以通过将文件下载到本地并运行,或通过使用 Docker 镜像和 Dockerfile,或者通过使用仓库中提供的 Kubernetes 清单来进行配置。对于我们的教程,我将使用 Kubernetes 清单,如下所示:

目前,UI 在 minikube 上工作得非常顺利,但如果你有 RBAC 集群,可能需要做一些调整,或者它也可能在没有任何调整的情况下直接工作。

$ kubectl create -f https://raw.githubusercontent.com/kubeless/kubeless-ui/master/k8s.yaml
serviceaccount "ui-acct" created
clusterrole "kubeless-ui" created
clusterrolebinding "kubeless-ui" created
deployment "ui" created
service "ui" created
$ minikube service ui -n kubeless

minikube 命令会弹出一个浏览器并打开 UI。UI 具有创建、编辑、调用和删除函数的功能,因此调用我们部署的函数。让我们在 textarea 请求中添加 {"term":"New York"} ,选择请求方式为 POST,然后点击运行函数。该函数将成功执行,并显示车站数据响应输出,如下截图所示:

我们也可以通过 Serverless Framework 调用相同的函数,函数将执行并获取所需的数据,如下代码所示:

$ serverless invoke --function bikesearch --data '{"term":"Albemarle"}' -l
Serverless: Calling function: bikesearch...
--------------------------------------------------------------------
[ { availableDocks: 12,
 totalDocks: 15,
 city: '',
 altitude: '',
 stAddress2: '',
 longitude: -77.079382,
 lastCommunicationTime: '2018-08-15 04:16:15 PM',
 postalCode: '',
 statusValue: 'In Service',
 testStation: false,
 stAddress1: 'Tenleytown / Wisconsin Ave & Albemarle St NW',
 stationName: 'Tenleytown / Wisconsin Ave & Albemarle St NW',
 landMark: '',
 latitude: 38.947607,
 statusKey: 1,
 availableBikes: 1,
 id: 80,
 location: '' } ]

Kubeless UI 仓库可以在 github.com/kubeless/kubeless-ui 查找。

Serverless 日志

如果出了问题会发生什么?我们没有设置错误处理机制,但可以通过在调用函数时传递错误来测试日志。所以,让我们在数据中加入错误并调用函数,如下代码所示:

$serverless invoke --function bikesearch --data '{"trm":"Albemarle"}' -l
Serverless: Calling function: bikesearch...

  Error --------------------------------------------------

  Internal Server Error

     For debugging logs, run again after setting the "SLS_DEBUG=*" environment variable.

  Get Support --------------------------------------------
     Docs: docs.serverless.com
     Bugs: github.com/serverless/serverless/issues
     Forums: forum.serverless.com
     Chat: gitter.im/serverless/serverless

  Your Environment Information -----------------------------
     OS: darwin
     Node Version: 6.10.3
     Serverless Version: 1.26.1

Serverless 返回了一个带有 500 服务器代码的错误消息,这正是你从一个 web 框架中所期望的。然而,为了更好地调试错误来源,查看 Python 堆栈跟踪信息会非常有帮助。那么,让我们获取日志来看看到底是什么错误:

$ serverless logs -f bikesearch
Hit Ctrl-C to quit.
172.17.0.1 - - [15/Aug/2018:20:17:18 +0000] "POST / HTTP/1.1" 200 460 "" "" 0/934928
172.17.0.1 - - [15/Aug/2018:20:17:18 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/133
172.17.0.1 - - [15/Aug/2018:20:17:48 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/72
172.17.0.1 - - [15/Aug/2018:20:18:18 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/108
172.17.0.1 - - [15/Aug/2018:20:18:48 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/123
172.17.0.1 - - [15/Aug/2018:20:19:18 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/74
172.17.0.1 - - [15/Aug/2018:20:19:48 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/138
172.17.0.1 - - [15/Aug/2018:20:20:18 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/75
172.17.0.1 - - [15/Aug/2018:20:20:48 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/149
172.17.0.1 - - [15/Aug/2018:20:21:18 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/187
172.17.0.1 - - [15/Aug/2018:20:21:48 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/147
172.17.0.1 - - [15/Aug/2018:20:22:18 +0000] "GET /healthz HTTP/1.1" 200 2 "" "kube-probe/." 0/71
172.17.0.1 - - [15/Aug/2018:20:22:47 +0000] "POST / HTTP/1.1" 200 2131 "" "" 1/232988
Traceback (most recent call last):
 File "/usr/local/lib/python2.7/dist-packages/bottle.py", line 862, in _handle return route.call(**args)
 File "/usr/local/lib/python2.7/dist-packages/bottle.py", line 1740, in wrapper
 rv = callback(*a, **ka)
 File "/kubeless.py", line 76, in handler
 raise res
KeyError: 'term'

KeyError 这一短语中可以清楚地看到,函数因错误的键名而失败,这给我们提供了了解出错原因的视角。但在类似生产的环境中,我们需要更为复杂的错误处理方法。

与 Jenkins 的持续集成

在本书中,我们已经研究了不同 Serverless 提供商的持续集成和持续部署,但就 Kubeless 而言(由于它仍在开发中),仍有许多需要改进的地方。在编写本书时,我发现,使用 Serverless Framework 时,我们只能在本地设置部署,前提是我们的 Kubernetes 集群(minikube)、Serverless Framework 和 Jenkins 已经在本地设置好。没有设置远程部署的功能。但随着 Serverless Framework 和 Kubeless 的成熟,这些功能会逐步添加。在接下来的教程中,我已经创建了文件以便在本地设置部署。

你可以通过阅读以下文章来了解如何设置远程部署:aws.amazon.com/blogs/opensource/running-faas-on-kubernetes-cluster-on-aws-using-kubeless/

如果你克隆了这个仓库:github.com/shzshi/kubeless-continuous-integration.git,那么你应该能够使用这个模板在本地设置持续集成。

然而,我们可以在笔记本电脑上本地运行这些文件,只要我们安装并配置了 Serverless Framework。让我们来看一下如何操作:

$ git clone https://github.com/shzshi/kubeless-continuous-integration.git
$ cd kubeless-continuous-integration

我们应该在这个文件夹里看到六个文件和一个目录,但在本教程中我们不会使用 Dockerfile 和 Jenkinsfile。它们可以在使用 Serverless Framework for Kubeless 实现远程部署后使用,示例如下代码:

$ npm install
$ npm test
> kubeless-nodejs@1.0.0 test /Users/shashi/Documents/packt/chapter7/kubeless-continuous-integration
> mocha ./test/*.js
kubelesshello
✓ should return 0 when "Hello Kubeless" is present
1 passing (8ms)

我们运行了npm install来获取 Serverless Framework 和 Node.js 应用程序所需的依赖项并进行测试。接着我们运行了 npm test,在这个过程中我创建了一个简单的单元测试来检查我们的函数是否正常,确保它们在部署到集群之前能够正常工作,如下代码所示:

$ serverless deploy -v
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Deploying function kubelesshello...
Serverless: Pods status: {"waiting":{"reason":"PodInitializing"}}
Serverless: Pods status: {"waiting":{"reason":"PodInitializing"}}
Serverless: Function kubelesshello successfully deployed
Serverless: Skipping ingress rule generation

因此,我们的 Node.js 函数将成功部署,我们可以在本地或通过 Kubeless UI 进行调用并进行测试。调用后,我们应该能够得到Hello Kubeless的输出,如下代码所示:

$ serverless invoke -f kubelesshello -l
Serverless: Calling function: kubelesshello...
--------------------------------------------------------------------
Hello Kubeless

监控 Kubeless

我们可以使用 Prometheus 监控kubeless函数。Kubeless 的运行时内建支持 Prometheus,运行时将自动收集每个函数的指标。Prometheus 将在默认仪表板上显示这些指标。

可以通过 Grafana 可视化 Prometheus 指标。Grafana 仪表板可以通过 Kubeless 提供的示例 JSON 文件进行配置:github.com/kubeless/kubeless/blob/master/docs/misc/kubeless-grafana-dashboard.json

Kubeless 的优缺点

在无服务器架构的世界中,Kubeless 才刚刚起步;它还有很长的路要走,才能成为像 AWS Lambda、Azure Function 或 Google Functions 这样的巨头。然而,它依然有不少优势,未来一定会成长为领先的 Serverless 框架之一。我们来看一下它的一些优点:

  • 无需服务器配置

  • 可以并行运行代码并轻松扩展

  • 不像其他服务提供商(如 AWS Lambda、Azure Function 或 Google Functions),它不是供应商独立的

  • 与无服务器部署框架的集成

  • 用于创建、更新、删除和调用函数的 UI

以下是它的一些缺点:

  • 函数的测试和集成仍然需要改进

  • 远程部署尚未到位

  • 使用大量内存的请求(状态、缓存、队列和持久化存储)由其他系统提供

  • 环境管理(开发、质量检查、用户验收测试或生产)没有文档记录或没有到位。

因此,在应用自动化方面,框架仍然需要大量改进,并且需要添加许多功能才能具备足够的功能性。

摘要

在本章中,我们了解了 Kubeless 无服务器框架。我们学习了如何设置 Kubeless,部署、移除和调用 Python 和 Node.js 函数。我们还学习了如何监控或记录这些函数。在下一章中,我们将学习使用无服务器技术的最佳实践,以及如何设置自动化和 DevOps。

第八章:DevOps 与无服务器的最佳实践及未来

在前几章中,我们了解了无服务器(serverless)是什么,以及无服务器功能的多个不同服务提供商。我们还探讨了如何构建、测试和部署无服务器应用程序,以及如何监控和记录执行过程。但是本章非常有趣,因为我们将进一步深入,学习一些关于无服务器的最佳实践。我们还将学习如何构建、部署、监控、记录和保护我们的无服务器应用程序的最佳实践。我们还将研究 DevOps 如何与无服务器一起工作。

DevOps 的重要方面

DevOps 的两个重要原则是自动化和流程。我们必须自动化从非生产环境到生产环境的每一部分开发,同时要保持持续的反馈,信息在各个环节之间往返流动,同时也要记录一切。让我们看看如何实现这一点的一些最佳实践。

协作与工具策略

DevOps 团队需要制定一个跨组织的统一工具策略,并且应该与不同团队(如开发、测试和基础设施)进行协作,共同商定 DevOps 的业务目标。团队之间应该实现无缝协作和集成。目标是实现一切自动化,因此理想的目标是从开发到生产的“一键部署”,并尽量减少人为干预。

敏捷开发

有许多敏捷方法论可供选择。Scrum、XP 和 Kanban 是其中一些更受欢迎的选择。你可以选择这些开发方法之一,给你提供一个更灵活的选项来规划、创造更快的输出,并在项目开发过程中保持清晰的聚焦和透明度。敏捷方法论有助于限制进行中的工作,这反过来有助于保持平衡的流程,以避免我们一次性做得过多。

版本控制所有内容

当我们谈到版本控制时,首先浮现在我们脑海中的往往是应用程序源代码的版本管理,这是我们非常熟悉的一项实践。但在源代码版本控制的基础上,我们还应该对数据库、构建产物、依赖项以及与应用程序相关的所有内容进行版本控制。这确保了应用程序的各个方面都可以通过存储库这一单一来源进行历史追踪。

捕获每一个请求

我们应该确保没有临时的工作或变更发生在 DevOps 之外,并且每个功能和非功能需求都应提出并维护变更请求。这些变更请求应该在工具中进行捕获和管理。

自动化测试和源代码分析

当我们谈论自动化测试时,这并不仅仅意味着测试的自动化。测试自动化应该包括预置测试数据并运行标准测试,以确保代码符合企业服务水平协议的标准。测试必须是持续的,应运行 100 次或 1,000 次,然后在成功完成后,自动提升到更高的环境。代码的质量应定期检查,并在代码覆盖率、源代码分析和代码性能未达到基准时返回给开发人员进行重构。

持续反馈

反馈循环是 DevOps 的一个非常重要的方面,它对 DevOps 成功起着至关重要的作用。反馈循环由开发过程中注册的一个问题与 DevOps 流程中的人类元素之间的自动化通信组成。它应由工具管理,通过该工具,问题必须手动或通过自动化机制进行注册。问题还应该标记一个工件,开发人员可以利用它追踪问题发生的原因、发生的时机以及发生的位置。该工具应有助于定义所有自动化和人类元素在循环中的沟通链条。

市场上线时间和周期时间

在 DevOps 流程中,我们将获得丰富的指标和实时报告,帮助我们衡量应用的市场上线时间和周期时间,因为这两个指标在我们将新功能提供给客户的速度和效率上起着至关重要的作用。市场上线时间衡量的是从头到尾将有价值的新功能推向市场的效率,而周期时间是衡量工程团队流程的指标,一旦功能定义完成,它就表示功能何时可以进入生产环境。它有助于理解团队的效率并加以改进。

日志指标

我们应始终跟踪 DevOps 流程的生产力,包括自动化和手动流程,并确定这些流程是否有利于组织。我们需要定义哪些指标与 DevOps 流程相关,比如部署速度、测试中发现的错误或构建时间。通过这个定义,自动化流程可以在没有人工干预的情况下解决这些问题。

我们已经简要介绍了可以应用于应用开发的一般 DevOps 实践,但当涉及到无服务器应用时,我们需要在工具和流程方面增加一些更具体的最佳实践。我们将深入了解每个服务提供商,并筛选出适用于无服务器应用的最佳实践,以及 DevOps 与无服务器架构的最佳实践。

无服务器架构的最佳实践

如我们所知,无服务器架构由一个称为“函数”的小段代码组成,函数运行在一个无状态的容器中。这个架构的一个主要目的是根据需要进行弹性伸缩。因此,考虑到这一点,我们的最佳实践几乎都集中在无服务器架构的这一方面。那么,让我们看看与无服务器概念相关的一些最佳实践。

一函数,一任务

当我们开始构建函数时,可能会遇到在代理路由后面有单体函数,并使用switch语句的情况。因此,如果我们只有一个或几个函数来运行整个应用,那么我们实际上是在对整个应用进行扩展,而不是扩展应用的某个特定部分。这是应该避免的,因为在这种情况下,扩展会成为问题,我们还可能最终得到大型且复杂的函数。

函数调用其他函数

我们应该避免在一个函数内调用另一个函数,因为这样会增加成本,且调试也会变得更加困难。我们失去了隔离函数的价值。如果需要的话,理想的做法是触发另一个函数来完成更多工作。

最小化库的使用

当我们编写一个函数时,可能需要一些库来让它运行。但将大量的库添加到函数中会让它变得非常慢。使用函数时有两种启动方式——一种是冷启动,即函数第一次启动,另一种是热启动,即函数已经启动并准备好从池中执行。因此,理论上来说,更多的库会使冷启动变慢,并且还会影响应用的扩展。随着规模的增加,函数必须执行冷启动,冷启动速度较慢,因此扩展速度也会变慢。

同时,随着库数量的增加,我们的代码安全性会降低,因此每添加一组库都必须进行安全测试。在实际使用之前,这些库还需要被信任。

使用 HTTP 时——每个路由对应一个函数

我们应该尽量避免在使用 HTTP 路由时使用单一函数代理,因为这会阻碍函数的扩展,并且还会让调试变得繁琐。有些情况下我们可以避免这样做,比如某些路由的功能与单一表格相关,并且与应用的其他部分解耦。

与 RDBMS 的数据库连接

无服务器应用的概念非常适合与服务配合使用,因此在函数中建立与 RDBMS 的连接可能会遇到问题,因为服务的各个部分仍然依赖于提供快速响应。

随着函数与 RDBMS 连接的规模增大,连接数也会随着规模增长,我们可能会引入瓶颈并在函数冷启动时出现 I/O 等待。

因此,无服务器架构让我们重新思考数据层。如果我们尝试将无服务器架构与现有的数据层(如 RDBMS)结合使用,那么你会发现整体应用性能会出现延迟。这也可能是为什么 DynamoDB 与无服务器架构结合得如此良好的原因。

使用消息和队列

当应用程序是异步时,无服务器应用程序运行非常高效,但对于 Web 应用程序来说,这并不简单,因为 Web 应用需要大量的请求–响应交互和查询。回到函数不直接调用其他函数这一点,重要的是要指出这就是如何将函数串联在一起的。队列在函数串联的场景中充当断路器,因此如果某个函数失败,我们可以轻松追踪到失败的队列并调查故障,这可能是由于推送失败消息到死信队列所导致的。

对于以无服务器架构为后台的应用程序,使用CQRS(命令查询责任分离)是理想的选择,它将输入和输出的概念模型在一个模型中分离成多个模型,用于更新和向用户显示变更。

数据流动

在无服务器系统中,数据处于流动状态。它通过系统流动,但最终可能进入数据湖。然而,在无服务器系统中,它应该保持流动状态,因此,确保将所有数据保持在流动中,而不是存储在数据湖中。在使用无服务器环境时,避免从数据湖中查询数据。虽然保持数据流动并非总是可能,但最好尽量做到。

测量规模

无服务器架构的主要特点是高效的扩展性,但我们应该以便于扩展的方式进行编码。我们可以避免在函数内部直接调用数据连接,或者避免添加大量的依赖库。简而言之,函数越轻量,冷启动越快,最终扩展也会更快。同时,确保在函数进入生产环境之前进行负载测试,以确保高效的扩展。

在本节中,我们探讨了使无服务器函数高效执行和扩展的各种最佳实践。虽然这不是完整的列表,但这些是一些非常重要的实践。在下一节中,我们将学习每个云服务提供商的最佳实践,并理解 DevOps 的最佳实践如何帮助加速无服务器开发和部署,使其更加高效。

AWS Lambda 的 DevOps 最佳实践与故障排除

到目前为止,我们已经学习了使用 DevOps 的最佳实践以及设计无服务器应用架构的最佳方式。我们还探讨了编写可扩展函数的最佳方法,并确保它们有更快的冷启动时间。接下来,我们将学习如何为特定云服务提供商应用最佳实践。

AWS Lambda 是一个非常流行且成熟的无服务器平台。正因如此,大多数无服务器的最佳实践都与 Lambda 保持一致,并且有许多工具和流程能够很好地将功能与 AWS Lambda 对接。在本节中,当我们谈论 DevOps 时,我们将关注源代码版本控制、构建、测试、打包、发布、监控、安全性以及成本控制等 DevOps 方面。这些 DevOps 的各个方面被整合在一起,作为一个统一的平台运行。如果我们想要达到 DevOps 的极乐境界,这需要我们付出大量的努力和耐心。

源代码版本控制

源代码版本控制是开发中的一个非常重要的部分。如果代码没有版本控制,那它就等同于不存在。现在有许多工具可用于源代码版本控制,但如今 Git 和 Apache Subversion 是最受欢迎的工具。通常,最好为每个应用创建一个代码库,然后为开发创建多个分支。关于 Git,当我们创建一个代码库时,已经有了一个主分支(master)。主分支保存着代码的黄金副本,通常用于基准参考,也可以作为生产副本。我们还可以为代码的功能、发布和开发版本创建其他多个分支,以实现高效的开发和部署。

因此,关于源代码管理的最佳实践是经常提交,并且每次开始工作时都确保发起拉取请求(pull request)。这样可以避免浪费时间进行代码合并,并且防止我们破坏构建。通常,我们有一个庞大的开发团队,分布在全球不同的地方,为应用构建不同的模块。如果我们某天忘记提交代码,那么第二天早晨我们就得浪费大量时间合并代码,才能开始开发。我们还应确保在提交前始终进行检查,否则我们可能会将一大堆垃圾、不需要的库、JAR 文件或调试文件提交到代码库,这会导致代码库被塞满,并且让我们的功能变得更庞大,最终降低性能。

我们还应该确保在提交代码时添加提交信息。评论应该解释我们为什么提交这段代码,因为这将帮助我们轻松追踪问题是否出现,也使得我们在提交了有 bug 的代码或某些功能发生变化时,能够更容易地回滚。当我们添加提交信息时,应该确保信息不太笼统或缺乏内容。我们还应确保它是固定的、有效的,并且没有拼写错误,因为如果我们引入了 bug 或问题,这将无法帮助我们追溯。除此之外,在前一次提交后再出现相同的提交信息也不是一种好习惯,因为我们通常提交代码是因为与前一次提交相比,代码发生了变化。

我们应避免将构建产物或编译的依赖项(如 .dll.jar)提交到源代码控制中,因为这可能会惹恼你的同事,因为他们需要检出大量文件,或者因为下载这些依赖项而导致本地环境被破坏。另一种避免提交问题的方法是编写预提交钩子,这可以缓解大多数提交问题。

我们已经谈到过版本控制,但我们还应该讨论版本数据库组件。像 UI、应用程序和测试代码一样,数据库组件也应该进行版本控制,以确保稳定的部署和应用。如果我们不对数据库进行版本控制,可能会导致应用程序使用旧数据或旧配置运行。通过版本控制,我们也能更容易地追踪特定版本发布中使用了哪些 DDL 和 DML 语句。

总之,我们应该始终记住定期提交,知道我们在提交什么,添加有效的提交信息,并确保我们自己执行这些操作。

AWS 还支持 Lambda 函数的版本控制。Lambda 函数通过发布来进行版本控制。AWS Lambda 控制台中有一个专门用于发布新版本的界面,我们可以为同一个函数创建多个版本。每个 Lambda 函数版本都有一个唯一的 ARN,并且发布后是不可变的。Lambda 还允许我们创建别名(如下图所示),这些别名指向特定的 Lambda 函数版本。每个别名将拥有一个唯一的 ARN,并且只能指向一个函数版本,而不能指向另一个别名:

让我们来看一个 AWS Lambda 版本管理的例子。假设我们有一个 AWS S3 桶,它是 Lambda 函数的事件源,因此每当桶中添加新的项目时,Lambda 函数会被触发并执行。事件源映射信息存储在桶的通知配置中,在该配置中,我们可以识别 S3 可以调用的 Lambda 函数 ARN。但是,新发布的版本不会自动更新,因此每当创建新版本的函数时,我们必须确保更新这个版本。

如果我们使用的是 Serverless 框架,新的版本是通过部署来创建的。还可以通过以下命令行回滚到先前的版本:

$ serverless rollback function -f my-function -v 23

构建

在编程术语中,构建是程序或产品的版本,但这个术语也用于指代 DevOps 的持续集成。因此,我们应该确保每次代码提交到 Git 仓库时都会触发构建。之所以这么做,是因为许多开发人员参与了这些项目,各自在自己的机器上独立工作,这可能没问题。但直到代码提交并在持续集成服务器上构建后,这将触发源代码分析和提交代码的单元测试,我们才能知道是否推送了有问题的代码。所以,确保每次提交时都触发构建是至关重要的。

成功构建的工件应始终进行版本控制。最佳做法是在 Nexus 仓库中对其进行版本控制,以便所有非生产环境的构建应推送到 Nexus 的快照仓库,而发布候选版本则应推送到 Nexus 的发布仓库。这个过程将帮助我们将发布构建与临时的快照构建分开,后者应该定期清除,因为每次构建时都会创建,并且以后不再需要。

构建应该触发源代码分析,这可以是针对 Node.js 和 Python 应用的 linting,如果你使用的是 Java 或 C#,那么也有许多工具可用于源代码分析。源代码分析确保开发人员遵循适当的编码标准。它还会执行安全检查并检查代码中的圈复杂度。这些工具还会生成报告,我们还可以为源代码分析的通过参数设置一个阈值,最终使构建通过。所以如果源代码分析检查的结果低于设定的基准,构建将会失败,开发人员必须修复问题。

如果我们使用 Java 作为无服务器架构的编程语言,那么有许多开源工具可供我们使用,例如 PMD、Checkmarx、Checkstyle、FindBugs 和 SonarQube。SonarQube 是一个流行的源代码分析工具,支持多种语言,例如 Java、C#、Node.js 和 Python。它还拥有一个漂亮的仪表板,安装和配置也非常简单。SonarQube 官方镜像已经发布在 Docker Hub 上。你可以设置并试试看。

你可以在 Docker Hub 上找到 SonarQube 镜像,地址是 hub.docker.com/_/sonarqube/

测试

测试是开发周期中的重要部分。测试确保无错误且安全的代码被部署到更高的环境,我们应该尽可能地将测试自动化。测试应与持续集成过程结合,并最终与持续部署相结合。测试的好处在于它为组织节省了大量时间和金钱,因为它能在开发初期阶段减少大多数 bug 和错误。测试有许多不同的层次,例如单元测试、集成测试、功能测试和性能测试。让我们看看这些层次的最佳实践。

单元测试

单元测试是测试软件时的基础层次。它基本上是指测试代码单元,以检查其功能的正确性。在单元测试过程中,代码在测试环境中使用模拟输入进行测试。然后将执行的输出与预期的输出进行比较,如果输出与预期输出匹配,则测试通过。

就无服务器概念而言,我们不需要担心 Lambda 函数、处理程序或事件;我们只需要组织好代码库,以便于单元测试的集成。如果你查看以下代码,你会看到我们将核心逻辑分离成独立的模块,而不是放在处理程序内,这样它们就可以单独进行单元测试:

const createUser = (event, context, callback) => { 
    const user = utils.RegisterUser(event.user) 
    const photoUrl = utils.updatePhoto(user) 
    callback(null, 
    { 
        statusCode: 200, 
        body: 'User Created!' 
    })
}

所以,最好将业务逻辑分离,使其独立于服务提供商、可重用,并且更容易进行测试。通过这种业务逻辑的分离,单元测试将更容易编写和运行,而且迁移到不同的提供商时也会花费更少的精力。

Node.js 和 Python 都有许多单元测试框架,但我们将要讨论的是 MochaJest。这两个框架都有可用的无服务器框架插件。

以下代码展示了 Mocha 无服务器插件的安装和使用。它可以作为服务本地安装,也可以包含在 serverless.yml 中,如下所示:

# serverless.yaml 
plugins:
  - serverless-mocha-plugin

同样,我们可以将 Jest 无服务器插件添加到 serverless.yml 中,如以下代码所示:

plugins:
  - serverless-jest-plugin
custom:
  jest:
    # You can pass jest options here
    # See details here: https://facebook.github.io/jest/docs/configuration.html
    # For instance, uncomment next line to enable code coverage
    # collectCoverage: true

同样,Nose 是一个流行的 Python 无服务器应用框架。

除此之外,我们还可以使用 LocalStack 来模拟 AWS 环境。LocalStack 帮助我们创建一个类似于 AWS 的本地测试环境,它为我们启动核心云 API,以便我们在本地测试我们的函数。

集成测试

集成测试基本上是指测试覆盖多个单元。我们的 Lambda 函数必须与第三方代码依赖项集成,而这些依赖项必须经过彻底的测试。这就是集成测试的作用所在。集成测试在 Serverless 世界中扮演着非常重要的角色,但如果你总是在云上进行测试,它的成本也很高。但我们可以通过模拟来减少成本,就像我们在看 LocalStack 框架时提到的那样。通过这种方法,我们可以在模拟大量 AWS 资源时加以利用。

类似地,我们可以使用 serverless invoke local 在本地调用 Lambda 函数,如果我们的函数需要从 DynamoDB 读取/写入数据,那么也可以使用 dynamodb-local Node.js 库来模拟。

尽管这些方法不能完全模仿 Lambda 的 100%,我们仍然能够快速找到代码库中的问题,而无需等待部署。

性能测试

性能测试是一种测试方法,用于检查我们的应用在预期工作负载下的表现。对于我们的应用类型而言,这是非常重要的测试类型,因为性能测试将为我们提供改善功能性能的机会,并帮助提升扩展速度。扩展是功能非常重要的特点,但我们有责任以一种有助于更快扩展的方式编写代码。这正是性能测试可以发挥作用的地方。

一个非常流行的性能测试工具是 Jmeter。它在行业中被广泛使用。Jmeter 压力测试将检查负载测试的关键绩效指标(KPI),例如响应时间、错误率、内存泄漏、缓慢、安 全问题和数据损坏。Jmeter 需要一个性能参数脚本。我们可以通过 BlazeMeter Chrome 扩展程序开始创建该脚本。

另一个流行的工具是 Artillery,它可以用于性能测试、负载测试和功能测试。它是一个开源工具,基于 Node.js 构建。它可以为应用生成负载,并模拟虚拟用户。你可以使用以下代码进行简单安装:

$ npm install -g artillery
$ artillery quick --count 10 -n 20 https://artillery.io/
Started phase 0, duration: 1s @ 21:13:07(+0100) 2018-08-28
Report @ 21:13:10(+0100) 2018-08-28
 Scenarios launched: 10
 Scenarios completed: 10
 Requests completed: 200
 RPS sent: 69.44
 Request latency:
 min: 28.3
 max: 248.4
 median: 41.9
 p95: 127.1
 p99: 191.7
 Codes:
 200: 200
All virtual users finished
Summary report @ 21:13:10(+0100) 2018-08-28
 Scenarios launched: 10
 Scenarios completed: 10
 Requests completed: 200
 RPS sent: 69.44
 Request latency:
 min: 28.3
 max: 248.4
 median: 41.9
 p95: 127.1
 p99: 191.7
 Scenario counts:
 0: 10 (100%)
 Codes:
 200: 200

还有一种结合了 Serverless Framework 和 Artillery 的工具,名为 serverless-artillery。这个包将帮助我们通过一个 Node.js 包来部署功能并运行性能测试,同时使其更容易集成到我们的 CI/CD 管道中。更多信息可以在以下链接找到://github.com/Nordstrom/serverless-artillery

测试非常重要,它能够在开发的早期阶段剔除缺陷和错误,但我们必须确保它在开发周期的早期引入,并且应一直执行到生产环境。但同样重要的是要自动化测试,并将其集成到持续集成和持续交付的周期中。然而,Serverless 测试也有其挑战,具体如下:

  • 无服务器架构实际上是独立分布式服务的集成,这些服务需要同时和独立地进行测试。

  • 我们可以在本地测试无服务器函数,但完全在本地模拟它们很困难,因此在云上测试它们同样重要。这个过程应该是自动化的。

  • 无服务器架构可以具有事件驱动、异步的工作流,彻底测试这些工作流并非易事。

监控

当我们想到监控时,首先浮现在我们脑海中的就是定期观察和追踪软件应用程序的操作和活动,并在系统或应用程序失败时设置触发报警。监控应该从开发的开始就启动,例如监控应用程序的速度或监控已开发的代码片段使用了多少资源。这个监控系统应与通知系统相结合,当内存使用超过定义的阈值时触发通知,比如发送电子邮件。

但是监控无服务器函数更加棘手。在服务器监控中,我们监控服务器的性能、网络延迟和 CPU,但这些细节对于无服务器而言是无关紧要的,因为基础设施完全由服务提供商管理。那么我们在无服务器中应该监控什么呢?我们仍然可以监控内存和并发率。尽管服务提供商处理 Lambda 函数的资源分配和执行,但仍然有内存使用和函数分配的并发执行次数的限制。

要执行无服务器监控,有很多工具可用。AWS 提供了几个开箱即用的工具,例如AWS CloudWatch,它将所有 AWS 资源输入到 AWS CloudWatch 中。CloudWatch 将是我们监控 Lambda 函数的第一个工具。CloudWatch 跟踪执行延迟、执行的函数数量以及执行过程中的错误等指标。但我们可以通过在 CloudWatch 中设置自定义指标来超越这些。我们还查看了与 Lambda 函数一起使用的 CloudWatch 仪表板。

AWS 为 Lambda 函数的应用性能提供了另一个强大的工具,那就是 X-ray。X-ray 是一个与 AWS Lambda 开箱即用集成的追踪工具。它提供了请求流动的端到端视图。通过 X-ray,我们可以分析 Lambda 函数及其关联服务的性能。我们可以识别并排除性能问题和错误的根本原因,并且通过应用程序的地图视图,我们可以看到它的所有组件。

Thundra 是另一个可以用作 AWS Lambda 应用程序性能监控工具的工具。它通过异步发布数据,将 CloudWatch 的数据通过函数和代理发送到应用程序中。它提供了一些非常好的图表,涵盖了许多指标,如调用次数、冷启动调用次数和持续时间、按错误类型和函数名称统计的错误次数等。

部署

要获取最新版本的 Lambda 函数进行执行,它们应该部署在云中。通过这种方式,如果我们的部署包更大,则部署会更慢。最终,当函数被调用时,解压缩过程将导致性能下降。我们的函数打包方式也会影响执行。例如,如果我们使用 Java,将所有依赖项打包成一个函数会比将依赖项放在lib文件夹中单独打包更慢。

有许多可用于部署的工具,但 Serverless Framework 是这项工作的主要候选者之一。当然,我们仍有许多事情需要完成,但这仍然比市场上任何其他开源选项更为成熟。

同样关于部署,应该进行编排,并且应通过大量的审批和变更请求流程,尤其是在 UAT 和生产环境中。生产环境中的部署应通过 ServiceNow 变更请求和 CAB 审批进行管理。

日志记录

Lambda 的运行时语言提供了一种机制,使您的函数能够将日志语句传递到 CloudWatch 日志中。我们需要让任何类型的应用程序都能够充分利用其日志,而不仅仅是无服务器应用程序。对于其他应用程序,我们可以从部署应用程序的服务器中检索日志。然而,对于无服务器应用程序,这是不可能的,因为没有服务器,而且目前我们无法“逐步执行”一个正在运行的 Lambda 函数。因此,我们在调查函数行为时,极度依赖我们创建的日志。因此,我们必须确保生成的日志具有适当的详细程度,能够帮助我们进行问题排查,同时又不会过度消耗计算时间。建议使用环境变量来创建一个loglevel变量。

适当使用日志级别可以确保我们在运行时仅在操作性排查期间选择性地承担额外的计算和存储成本,供我们的函数参考,从而确定在运行时应创建哪些日志语句。

安全性

安全性应该是我们设计和实现函数时的首要优先事项。无服务器应用和基于服务器的应用之间的一个主要区别是,在无服务器应用中,我们无法控制服务器,因此我们必须将大部分安全措施嵌入到代码中。因此,在编写无服务器应用时,我们应该确保编写安全的代码,以验证所有第三方的安全配置。让我们来看看一些实现安全代码所必需的最佳实践。

每个函数一个 IAM 角色

建议为每个函数创建一个角色,因为这将解耦 IAM 角色。这还将使我们能够为各个函数提供最小权限。例如,假设一个函数在其代码中使用 KMS 密钥。如果我们为 KMS 密钥使用一个公共角色,那么所有其他函数都将能够访问该 KMS 密钥。

不使用长期凭证

在 Lambda 函数代码中使用临时 AWS 凭证始终是安全的。这时,静态分析配置起着重要作用。最好通过 AWS SDK 在函数代码中创建一个 AWS 服务客户端,而不提供任何凭证。SDK 应该自动管理凭证的获取和轮换,适用于分配的角色。

不持久化秘密信息

最佳实践是不持久化秘密信息。然而,我们的函数可能需要某些长时间有效的秘密信息,例如数据库凭证和依赖服务的访问密钥。由于这一需求,建议对这些秘密信息进行加密。我们有几种选择,例如使用带加密的 Lambda 环境变量和 Amazon EC2 系统管理器的参数存储。

秘密信息不应保存在内存中或持久化。相反,函数应该获取临时凭证并保持其轮换,定期撤销它们。与 Lambda 函数的 API 交互应该经过身份验证和授权。

Lambda 在 VPC 中

在 VPC 中使用 Lambda 函数时,我们应该遵循使用网络安全的最佳实践,通过使用最小权限的安全组、特定于 Lambda 函数的子网和路由表来允许特定于 Lambda 函数的流量,如果我们使用了来自 VPC 的资源。

AWS Lambda 最佳实践

到目前为止,我们了解了关于 AWS Lambda 的 DevOps 最佳实践,但我认为本章不能结束,而不讨论 Lambda 函数的最佳实践。这些最佳实践最终将帮助我们开发高效的 Lambda 函数,并帮助我们实现更快的部署。让我们逐一来看这些最佳实践。

保持处理程序独立于业务逻辑

通常建议将业务逻辑放在处理程序之外,因为这样可以将我们的业务逻辑与 lambda 函数的运行环境解耦,重用业务逻辑函数,并且使我们更容易测试编写的单元测试,正如我们在本章早些时候讨论的那样。

保持温暖容器的存活

在编码时,我们必须确保我们的代码编写方式能够利用温暖容器的资源,而不是进行冷启动。这意味着我们需要对变量的作用域进行设计,使其在随后的调用中尽可能多次重用。我们应该重用在先前调用中建立的连接(例如 HTTP 或数据库连接)。

依赖控制

Lambda 在调用时需要许多库。Lambda 会始终寻找最新的库和安全更新,但这也会带来 lambda 函数行为的变化。因此,最好将依赖项与函数一起打包并部署。为了提高性能,这些依赖项也应该在低环境中进行控制和管理。

缩短依赖的超时

我们需要为所有外部依赖配置较短的超时时间。我们不能允许它们在持续查找依赖时继续运行,因为 Lambda 是按函数执行的持续时间计费的。执行时间越长,费用就越高。

异常处理

我们应该评估函数的故障行为应该是什么,因此,我们的函数应抛出正确的异常,以便更快地解决问题。

递归编程

我们应避免在 lambda 函数中使用递归代码,因为函数会递归调用自身直到满足某个条件。但这会导致多次调用并增加成本。如果我们不小心这样做了,那么我们应该将函数的并发执行限制设置为 0,以便在更新代码时立即限制所有对该函数的调用。

高可用性

当我们进入生产环境时,高可用性变得至关重要,对于无服务器应用程序而言,高可用性取决于 lambda 函数能够执行的区域数量。如果我们的函数使用默认网络,那么它会自动在该区域内的所有可用区域中执行。无需做其他配置来为函数在默认网络环境中配置高可用性。因此,在设计 VPC 时,重要的是要包括来自多个可用区的子网。

运行时语言

在设计无服务器应用程序时,选择运行时语言是相当棘手的。但当我们决定运行时语言时,我们总是可以依赖我们的技能组合来获得一些舒适感。不过,运行时语言也决定了我们应用程序的性能。例如,如果我们选择 Java 或 .NET(编译语言)作为运行时语言,那么容器的首次调用会产生最大的启动成本,但随后的调用性能会有所提升。相比之下,像 Node.js 或 Python 这样的解释型语言,初次调用时间非常快,但与编译型语言相比,它们无法输出最大性能。因此,如果应用程序对延迟敏感或负载波动较大,建议使用解释型语言;而如果应用程序的流量没有大幅波动,或者用户体验不受 Lambda 函数响应时间的影响,那么我们可以选择任何语言作为运行时语言。

针对冷启动和热启动的代码

Lambda 函数的性能取决于我们在其中编写的逻辑和我们调用的依赖项。因此,编写 Lambda 函数的最佳实践是考虑容器的冷启动和热启动。

为了提高热启动容器的性能,确保将任何外部化的配置或依赖项存储并引用到本地,以便你的代码能快速获取。限制每次调用时对全局和静态变量的变量/对象重新初始化。保持 HTTP 和数据库连接的活跃状态。

为了更好的冷启动容器性能,最好使用默认的网络环境,除非需要通过私有 IP 与 VPC 内的资源连接。选择解释型运行时语言而非编译型语言。尽量保持函数代码包尽可能小,因为这将减少从 S3 桶下载、解压代码并调用它的时间。

成本优化

假设函数使用的最小资源大小能够提供最低的总成本是一种反模式。如果你的函数资源大小太小,可能因为执行时间过长而支付更多费用,而如果使用更多的资源,函数可以更快完成,反而可能节省成本。

你不需要通过一系列阻塞/同步的 API 请求和响应来实现所有的用例。如果你能够将应用程序设计为异步的,你可能会发现架构中的每个解耦组件执行其工作所需的计算时间比那些紧密耦合并花费 CPU 周期等待同步请求响应的组件要少。许多 Lambda 事件源与分布式系统非常契合,并且可以以更具成本效益的方式将你的模块化和解耦函数集成在一起。

一些 Lambda 事件源允许您定义每次函数调用时传递的记录数的批量大小(例如,Kinesis 和 DynamoDB)。您应该进行测试,找到每个批量大小的最佳记录数,以便调整每个事件源的轮询频率,使其与您的函数完成任务的速度相匹配。

可供 Lambda 集成的各种事件源意味着您通常有多种解决方案可以满足您的需求。根据您的用例和需求(请求规模、数据量、所需延迟等),根据选择的 AWS 服务组件,您的架构的总体成本可能会有显著不同。

Azure 函数的最佳实践

对于其他云提供商(如 Azure),最佳实践应与 Lambda 函数保持一致。在本节中,我们将介绍一些与 Lambda 函数密切相关的 Azure 最佳实践。

避免大型且长时间运行的函数

大型且长时间运行的函数可能会导致意外的超时问题,且函数可能因为许多 Node.js 依赖而变得庞大。特别是导入依赖项可能还会导致加载时间增加,从而引发意外的超时。

在可能的情况下,我们应将大型函数重构为较小的函数集,使其协同工作并快速返回响应。

跨函数通信

如果我们没有使用持久化函数或逻辑应用程序来集成多个函数,通常的最佳实践是使用存储队列进行跨函数通信。存储队列更便宜且更易于配置。

存储队列中的单个消息大小限制为 64 KB。如果我们需要在函数之间传递更大的消息,可以使用 Azure 服务总线队列来支持消息传递。

函数应保持无状态

推荐尽可能使函数保持无状态且幂等。将任何必要的状态信息与您的数据关联。例如,正在处理的订单可能会有一个相关的状态成员。函数可以根据该状态处理订单,同时函数本身保持无状态。

非常重要的是,推荐幂等函数使用定时触发器。例如,如果您有必须每天运行的任务,我们应该编写使其可以在一天中的任何时间运行并得到相同结果的代码。如果某一天没有工作,函数可以跳过。另外,如果上次运行未能完成,下一次运行应该从上次停止的地方继续。

函数应具有防御性

设计我们的函数时,最好具备在下一次执行时从上次失败点继续执行的能力。建议编写具有适当错误处理的函数,考虑到网络故障、配额限制或其他错误。所有这些问题都可能随时影响我们的函数。我们需要设计函数以应对这些情况。

假设我们的代码在将 2000 个项目插入队列进行处理后失败,我们应该能够追踪已完成的项目集合,否则我们可能会再次插入这些项目。

如果在将 5000 个项目插入队列进行处理后发生故障,您的代码如何反应?你应该追踪已完成的项目集合。否则,下次可能会再次插入这些项目。这可能会严重影响你的工作流程。

同一个函数应用中不应同时包含测试和生产的代码

我们不应在同一个函数应用中混合测试和生产函数或资源,因为函数应用中的函数共享资源,比如内存共享,这可能导致性能下降。因此,我们在生产环境的函数应用中加载的内容要小心,因为内存会在函数应用内的每个函数间进行平均分配。

使用异步代码,但避免阻塞调用

异步编程是推荐的最佳实践。然而,始终避免引用 Result 属性,或者在 Task 实例上调用 Wait 方法。这种做法可能会导致线程耗尽。

配置主机行为以更好地处理并发性

host.json 文件在函数应用中用于配置主机运行时和触发器行为。除了批处理行为外,你还可以管理多个触发器的并发性。通常,调整这些选项中的值有助于每个实例根据调用的函数的需求进行适当的扩展。

hosts 文件中的设置适用于应用中的所有函数,且在函数的 单实例 中生效。例如,如果你有一个包含两个 HTTP 函数的函数应用,并将并发请求设置为 25,那么对任何一个 HTTP 触发器的请求都会计入共享的 25 个并发请求。如果该函数应用扩展到 10 个实例,那么这两个函数实际上将允许 250 个并发请求(10 个实例 X 每个实例 25 个并发请求)。

Google Functions 的最佳实践

即使服务提供商不同,大多数最佳实践在无服务器函数中是相同的,但我仍然想在这里列出一些。

编写幂等函数

函数应该在多次调用后仍能产生相同的结果。这有助于在先前的调用在代码执行过程中失败时重试调用。

信号函数调用完成

发出函数完成的信号,未能这样做可能导致函数执行直到超时为止。如果发生超时,您将为整个超时时间付费。超时也可能导致后续调用需要冷启动,从而增加延迟。

不要启动背景活动

函数调用一旦发出终止信号即结束。终止后的任何代码无法访问 CPU,也无法继续执行。此外,当在同一环境中执行后续调用时,您的后台活动会恢复,干扰新的调用。这可能导致意外行为和难以诊断的错误。函数结束后访问网络通常会导致连接被重置(以及ECONNRESET错误代码)。

背景活动是指函数终止后发生的任何事情。通常可以通过检查日志中在调用结束后记录的任何内容来检测背景活动。背景活动有时可能隐藏在代码更深处,特别是在存在回调或定时器等异步操作时。请检查代码,确保所有异步操作在终止函数之前完成。

始终删除临时文件

临时目录中的本地磁盘存储是一个内存文件系统。您写入的文件会占用可用内存,并且有时会在调用之间保持存在。如果没有显式删除这些文件,最终可能会导致内存不足错误,并触发随后的冷启动。

您可以通过在 GCP 控制台中选择函数列表中的函数(console.cloud.google.com/getting-started)并选择内存使用情况图,查看单个函数使用的内存。

不要尝试在临时目录之外写入文件,并确保使用平台/操作系统独立的方法来构建文件路径。

您可以通过使用pipelining绕过临时文件的大小限制。例如,您可以通过创建读取流,传递给基于流的处理,并将输出流直接写入云存储来处理云存储中的文件。

本地开发

函数部署需要一些时间,因此通常在本地使用shim测试函数代码会更快。

错误报告

不要抛出未捕获的异常,因为它们会导致后续调用时发生冷启动。有关如何正确报告错误,请参阅错误报告指南cloud.google.com/functions/docs/monitoring/error-reporting)。

使用 SendGrid 发送电子邮件

Cloud Functions 不允许通过端口 25 进行外部连接,因此无法与 SMTP 服务器建立非安全连接。您应该使用SendGrid来发送电子邮件。

明智地使用依赖项

因为函数是无状态的,执行环境通常会从头开始初始化(在冷启动期间)。当冷启动发生时,函数的全局上下文被评估。

如果您的函数导入模块,则这些模块的加载时间可能会增加冷启动时的调用延迟。通过正确加载依赖项并不加载函数不使用的依赖项,可以减少此延迟以及部署函数所需的时间。

使用全局变量来在未来调用中重用对象。

不能保证云函数的状态将被保留以供将来调用。然而,云函数通常会重用先前调用的执行环境。如果您使用全局范围声明变量,则其值可以在后续调用中重用,而无需重新计算。

通过此方式,您可以缓存可能在每次函数调用时昂贵重新创建的对象。将这些对象从函数体移动到全局范围可能会显著提升性能。

对全局变量进行惰性初始化

如果您在全局范围内初始化变量,则初始化代码将始终通过冷启动调用执行,增加函数的延迟。如果某些对象在所有代码路径中都未使用,则考虑按需惰性初始化它们。

总结

正如其名称所示,本章节主要介绍了在使用 DevOps 构建无服务器架构时应用的故障排除技术和最佳实践。在下一章中,我们将讨论 AWS Lambda、Azure Functions 以及开源版本的各种用例,以及 DevOps 如何适应其中。

第九章:使用案例和附加功能

在上一章中,我们学习了如何应用与无服务器函数和 DevOps 相关的一些最佳实践。我们理解了构建轻量且安全的无服务器函数有多么重要。我们还学习了如何监控和记录无服务器应用程序的一些最佳实践。那么,接下来,让我们看看无服务器架构如何在实际应用中被使用,并学习如何围绕它实现端到端的 DevOps。

随着开发者们从单体应用转向无服务器架构,越来越多的使用案例开始出现。但与此同时,构建、测试和部署它们变得更加困难。因此,在本章中,我们将学习一些无服务器的使用案例,并学习如何设置端到端的部署管道。

本章将涵盖以下主题:

  • AWS Lambda 使用案例和附加功能

  • Azure Functions 附加功能

  • Google Functions 附加功能

AWS Lambda 使用案例和附加功能

到目前为止,我们的旅程是关于 AWS Lambda 的介绍,如何构建、测试、部署、监控和记录它。我们还研究了一些 AWS Lambda 和 DevOps 的最佳实践。那么,现在在本章中,是时候将它们汇聚成一个使用案例了。在我们讨论与 AWS Lambda 相关的 DevOps 使用案例之前,让我们先看看 Lambda 可以高效使用的一些案例,然后再看看 DevOps 如何为开发者和业务用户简化工作。

AWS Lambda 使用案例

如我们所知,无服务器架构的引入是为了帮助企业专注于应用开发,而不必担心底层服务器。通过降低成本和缩短部署周期,无服务器架构的采用率正在呈指数级增长。现在,很多公司采用无服务器架构作为确保快速自动扩展和缩减的方式。让我们看看一些无服务器架构在行业中被广泛采用的有用应用场景。

无服务器网站

对于无服务器架构的最佳采用模型是那些希望利用 AWS Lambda 和 AWS S3 的网站。AWS 提供的这两项资源都非常便宜。而且,使用 AWS Lambda 时,它可以按需扩展并在需求减少时缩减规模。因此,它们不需要为始终保持服务器运行而支付大量费用。我们可以考虑将静态内容或前端托管在 S3 存储桶中,这些前端应用程序可以通过 API 网关的 HTTPS 端点向 Lambda 函数发送请求,Lambda 会完成应用逻辑的重负担,并将数据持久化到完全托管的数据库服务中(RDBMS关系数据库)或 DynamoDB非关系数据库))。我们可以将 Lambda 函数托管在 VPC 中,将其与其他网络隔离,这样也能降低成本,因为我们只需支付 AWS S3 和 AWS Lambda 所产生的流量费用,以及数据库服务的额外费用。

视频和图像处理

随着 Instagram 越来越流行,图像和视频的应用变得更加广泛,但处理或调整大小同样需要较高的成本。传统的编辑视频和图像的方式是托管一个始终运行的虚拟机,当图像或视频需要编辑时,再在虚拟机内运行相应的应用程序。但我们不得不承担 24/7 运行虚拟机的成本,即使它并不总是被使用。无服务器计算可能是这类任务的最佳选择。无服务器服务可以用来动态调整图像大小或更改视频转码,以适应不同目标设备的需求。

此外,使用无服务器计算,我们可以接入 Google Vision API 或 Amazon Recognition 来识别面部和图像,或屏蔽不当内容。在 AWS 章节中,我们创建了一个教程,其中图像上传到一个 S3 存储桶,当图像上传到该 S3 存储桶时,Lambda 会自动触发,将图像调整大小并推送到另一个存储桶。

日志处理与通知

我们可以使用 Lambda 函数从 CloudTrail 和 CloudWatch 获取日志,Lambda 会监控特定的触发器和日志条目,并在事件发生时调用 SNS 通知。这些通知可以配置并发送到 Slack、Jabber 或其他支持系统。

控制物联网

物联网IoT)是市场上的最新趋势,许多家庭和办公室的智能设备可以通过 Alexa 控制。但它们是如何工作的呢?正是 Alexa 与 Lambda 联手控制这些智能设备。比如我们要点亮一个由 Alexa 通过语音消息控制的 LED 灯泡,事件会在 Alexa 上触发,然后调用 Lambda 函数执行开关灯的操作。所以,无服务器计算可能会成为所有物联网调用的后端。

备份和日常任务

Lambda 函数在备份和日常任务中扮演着重要角色。我们可以调度 Lambda 函数来处理重复性任务,比如检查空闲资源、创建备份、生成日报和其他日常工作。传统方式是安装一个始终运行在服务器上的任务管理应用程序,按照需求执行任务。但使用 Lambda 后,函数仅在特定事件或时间表下被调用。它们在需要时也会水平扩展,而且成本更低。

持续集成与持续部署

快速迭代软件的能力比以往任何时候都更为重要。CI/CD 流水线允许你以小幅度更新代码,这样就可以每天发布 bug 修复和其他更新。

无服务器计算可以自动化许多这些过程。代码提交可以触发网站构建和自动重新部署,或者拉取请求(PR)可以触发自动化测试,确保代码在人工审核之前经过充分测试。

当你考虑与无服务器应用相关的自动化可能性时,去除工作流程中的人工任务变得更加简单。

所以,我们已经看到了不同的使用场景,在这些场景中,我们可以使用 AWS Lambda 函数,它们能够比传统的服务器或虚拟机执行更好的工作。目前,现实生活中使用无服务器架构的数量较少,但随着时间的推移,这种情况肯定会在未来几年增长。

但是,随着无服务器架构的使用增加,管理它们变得非常繁琐。例如,如果我们将无服务器架构用于股票市场门户网站或任何票务应用程序,那么这些应用程序将有许多功能需要协同工作,因为应用程序和功能必须进行版本控制、构建、测试、部署和回滚。这就是持续集成和持续交付发挥作用的地方。因此,我们的 DevOps 使用案例将是如何为 Lambda 函数设置高效的 CI/CD。

从本地开发开始,serverless 框架提供了一个有趣的插件——serverless offline。这个插件在本地模拟 AWS Lambda 和 AWS API 网关,帮助我们在彻底测试代码之前,在本地开发和测试代码,而无需先上传到 AWS 云。

为了演示这一点,我使用了我们之前的任务管道并进行了修改,使其能够在本地节点上与 DynamoDB 的无服务器离线模式一起使用。有趣的是,AWS 提供了一个可下载版本的 DynamoDB 文件,允许我们在本地开发环境中设置 DynamoDB 服务器。因此,这使得我们可以在本地开发 DynamoDB 应用程序,而无需为数据存储和数据传输付费,而且也不需要互联网连接。你可以在以下链接找到更多关于如何设置的信息:docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html

我仅仅使用了 serverless-dynamodb-local 插件名称来在本地进行设置。与 Jenkins 一起设置起来更容易。我在 Jenkinsfile 中创建了一个额外的阶段。因此,在这个阶段,我正在安装 serverless-dynamodb-localserverless-offlineserverless-mocha-plugin,这些插件是我们在将函数推送到 AWS 云之前进行单元测试所需要的。我已经为此添加了代码片段,并将代码放入 Git 仓库中(github.com/shzshi/aws-lambda-devops-usecase.git):

#Jenkinsfile

stage ('Dev init')
{
    nodejs --version
    npm install serverless-dynamodb-local --save-dev
    npm install serverless-offline --save-dev
    npm install serverless-mocha-plugin --save-dev
    npm install
    serverless dynamodb install
    chmod 755 startOffline.sh
    chmod 755 stopOffline.sh
}

在下一个 Jenkins 阶段,我正在本地安装 DynamoDB,并将本地 API 任务端点的路径添加到 Jenkins 作业中,并调用无服务器测试。我已经更新了 serverless.yml 配置文件,并确保在 Jenkinsfile 中更新了 TASKS_ENDPOINT,因此其余的管道应与第三章中应用 DevOps 于 AWS Lambda 应用程序的内容相同:

# serverless.yml
plugins:
    - serverless-dynamodb-local
    - serverless-offline
    - serverless-mocha-plugin
custom:
    serverless-mocha-plugin:
        preTestCommands: 
            - bash startOffline.sh
        postTestCommands:
            - bash stopOffline.sh
    dynamodb:
      stages:
        - dev
      start:
        migrate: true
# jenkinsfile
stage ('Dev init')
        {
            steps {

                deleteDir()

                checkout scm

                sh '''
                    nodejs --version
                    npm cache clean --force
                    npm install serverless-offline --save-dev
                    npm install serverless-mocha-plugin --save-dev
                    serverless plugin install -n serverless-dynamodb-local
                    npm install
                '''
            }
        }

        stage ('System Test on Dev') {

             steps {
                sh ''' 
                    sls dynamodb remove
                    serverless dynamodb install
                    chmod 755 startOffline.sh
                    chmod 755 stopOffline.sh
                    export TASKS_ENDPOINT=http://localhost:3000
                    serverless invoke test
                '''
                }
        } 

Jenkins 日志应该类似于下面这样:

Installation complete!
+ chmod 755 startOffline.sh
+ chmod 755 stopOffline.sh
+ export TASKS_ENDPOINT=http://localhost:3000
+ serverless invoke test
Serverless: Tests being run with nodejs6.14,  service is using nodejs6.10\. Tests may not be reliable.
Serverless: Run command: bash startOffline.sh

  Create, Delete
    ✓ should create a new Task, & delete it (198ms)

  Create, List, Delete
    ✓ should create a new task, list it, & delete it (180ms)

  Create, Read, Delete
    ✓ should create a new task, read it, & delete it (133ms)

  3 passing (523ms)

一旦管道成功,应该如下图所示:

所以,现在我们可以将函数作为持续集成的一部分在本地开发和测试,然后将其迁移到云端用于其他非生产和生产环境。但有一件事可以进一步自动化,就是我们在云中的 Lambda API 路径,因为当函数部署到 AWS 云时,我们会得到 API 网关的随机主机名:

api keys:
  None
endpoints:
  POST - https://ax1hlqv0vl.execute-api.us-east-1.amazonaws.com/sit/mytasks
  GET - https://ax1hlqv0vl.execute-api.us-east-1.amazonaws.com/sit/mytasks
  GET - https://ax1hlqv0vl.execute-api.us-east-1.amazonaws.com/sit/mytasks/{id}
  PUT - https://ax1hlqv0vl.execute-api.us-east-1.amazonaws.com/sit/mytasks/{id}
  DELETE - https://ax1hlqv0vl.execute-api.us-east-1.amazonaws.com/sit/mytasks/{id}
functions:

我们已在 Jenkins 流水线 Jenkinsfile 中更新了 API 主机名的任务端点。之所以这么做,是因为 API 网关提供的主机名不是静态的,而且每次我们重新部署或删除服务时,主机名会发生变化,因此我们必须在任何使用该主机名的地方进行更改。那么,如何解决这个问题呢?通过动态分配静态域名,这可以通过 Serverless 框架及其插件serverless-domain-manager轻松实现。让我们看看如何实现这一点。

在我们开始使用插件之前,有几个前提条件:

一旦前提条件成功完成,我们需要安装 Serverless 域管理插件:

$ npm install serverless-domain-manager --save-dev

然后,在serverless.yml文件中,我们添加两个部分。首先,我们在插件区块中添加serverless-domain-manager,然后通过Custom区块配置插件的customDomainbasePath属性将被添加到我们服务的每个路由前缀。例如,如果我们的函数是helloworld,并且我们将basePath设置为serverless,那么注册为/helloworld的路由将通过serverless/helloworld访问。

例如,https://api.<registered_domain_name>/serverless/helloworld:

plugins:  
    - serverless-domain-manager

custom:  
    customDomain:  
        domainName: 'api.<registered_domain_name>'  
        basePath: '<my_base_path>'  
        stage: ${self:provider.stage}                                     createRoute53Record: true

我们还可以通过在 Jenkins 流水线中集成 SonarQube 或 ESlint 来设置源代码分析。

监控

当我们谈论监控无服务器应用程序时,我们不需要担心 CPU 使用率或内存使用率,也不需要更新我们的系统包,因为所有这些都由 AWS 管理。但 Lambda 函数仍然需要监控执行失败,因为在生产环境中,一次函数失败可能会带来灾难。

默认情况下,CloudWatch 为 Lambda 函数提供度量指标,这些指标包括:

  • 调用次数:函数被调用的次数

  • 错误:由于各种错误、超时、未处理的异常、内存问题和其他问题导致函数失败的次数

  • 限制:函数被限制的次数,AWS 限制了每个函数的并发执行次数,如果超过限制,函数将被限制执行

  • 时长:函数的调用时间

错误和限制需要 24/7 监控,我们不能一直盯着 CloudWatch 看,但我们可以为所有的错误和限制设置警报。如果我们使用 serverless 框架,可以通过一个名为serverless-plugin-aws-alerts的插件来管理。这使得为服务设置警报变得非常简单。

要设置警报,我们需要在 serverless 框架服务中安装该插件:

$ npm install serverless-plugin-aws-alerts --save-dev

然后,我们需要将其添加到插件部分,并在serverless.yml的 custom 部分设置警报详情。这样,部署到生产阶段时,所有函数都会添加警报。接着,我们为 SNS 主题配置订阅,每个订阅必须有一个协议,在我们的案例中是电子邮件,端点则是电子邮件地址:

# serverless.yml
plugins: 
    - serverless-plugin-aws-alerts 
custom:  
    alerts:  
        stages:  
            - production
         topics:  
             alarm:  
                topic: ${self:service}-${opt:stage}-alerts-alarm
                notifications:  
                    - protocol: email  
                      endpoint: myemail@domain.com   
                alarms:  
                    - functionErrors  
                    - functionThrottles

静态网站

AWS Lambda 函数将作为我们网页应用的支柱。我们还需要一个外观前端,和 Lambda 函数一起部署,并且有一个可用的 serverless 框架插件,可以轻松完成这个任务。这个插件是serverless-finch。它会将我们网页应用的静态资源上传到 AWS S3。

让我们来看看它是如何工作的。要使用这个插件,我们首先需要安装它,然后将其配置到我们的serverless.yml文件中:

$ npm install --save serverless-finch

所以,一旦插件安装完成,我们首先需要创建一个分发文件夹,默认情况下为client/dist,也可以通过custom标签进行配置。所以,所有的静态内容将存放在client/dist文件夹中:

custom:
  client:
    ...
    distributionFolder: [path/to/files]
    ...

接下来,我们需要在 custom 标签中提到 S3 桶的名称,静态文件将上传到该桶:

custom:
  client:
    bucketName: [unique-s3-bucketname]

所以,理想情况下,serverless.yml应如下所示,在这里我们定义了提供者,添加了serverless-finch插件的配置,并通过custom标签定义了客户端详情。indexDocument参数用于索引页面,errorDocument参数用于错误页面:

service: my-static-website

frameworkVersion: "=1.26.0"

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: us-east-1
  profile: dev-profile

plugins:
  - serverless-finch

custom:
  client:
    bucketName: my-static-pages-bucket
    distributionFolder: client/dist 
    indexDocument: index.html
    errorDocument: index.html

最后,要部署内容,我们需要运行以下命令,并应该能够在 serverless 应用的控制台输出中看到新部署网站的位置:

$ serverless client deploy 

这个插件还提供了许多其他的配置参数和功能。你可以通过以下链接找到它们:github.com/fernando-mc/serverless-finch

热身

冷启动是与无服务器函数相关的主要问题之一。函数越温暖,性能越好。但我们如何确保我们的函数始终保持热启动状态呢?Serverless 框架提供了一个插件来帮助我们做到这一点。该插件的名称是 Serverless WarmUP Plugin。那么,这个插件是如何工作的呢?

插件创建了一个调度事件 Lambda,该 Lambda 在指定的时间间隔内调用我们选择的所有服务 Lambdas。因此,该插件通过强制使底层容器保持活动状态来保持函数的热身。要设置此插件,我们首先需要安装它:

$ npm install serverless-plugin-warmup --save-dev

然后在 serverless.yml 中,我们可以通过在插件部分调用它来配置它。我们需要提到应该运行哪个环境的预热,可以是单个或多个。然后在自定义部分,我们需要定义要运行的预热 Lambda 函数的配置。它可以进一步配置,有关这些配置的详细信息可以在链接找到:github.com/FidelLimited/serverless-plugin-warmup

# serverless.yml
plugins:
  - serverless-plugin-warmup
functions:
  hello:
    warmup:
      - production
      - staging
custom:
  warmup:
    folderName: '_warmup' // Name of the folder created for the generated warmup 
    cleanFolder: false
    memorySize: 256
    name: 'warm-my-lambdas'
    role: myCustRole0
    schedule: 'cron(0/5 8-17 ? * MON-FRI *)' // Run WarmUP every 5 minutes Mon-Fri between 8:00am and 5:55pm (UTC)
    timeout: 20
    prewarm: true // Run WarmUp immediately after a deploymentlambda
    tags:
      Project: foo
      Owner: bar  

这些是一些 Lambda 函数的附加组件,可以帮助我们更轻松地创建和维护无服务器函数。

Azure 函数的附加组件

在 DevOps 中,确保开发更顺畅、更快速,并且将无错误的应用程序部署到生产环境非常重要。如果我们能够在本地开发、调试和测试应用程序,那将是可能的。但是就 Azure 函数而言,为了测试和调试函数,我们需要每次将其部署到 Azure 云中。好消息是,像 AWS Lambda 一样,Azure 函数可以通过 Azure 函数核心工具 在本地进行调试和测试。

使用 Azure 函数工具,我们可以在本地创建、开发、测试、运行和调试 Azure 函数。它们可以安装在 Windows、macOS 和 Linux 上。我们可以访问 GitHub 链接了解如何设置它们:github.com/Azure/azure-functions-core-tools

安装完工具后,我们需要首先创建create function应用程序。默认情况下,这也会创建一个本地 Git 仓库,可以通过传递参数 -n 来跳过:

$ mkdir my-local-azure-function
$ cd my-local-azure-function
$ func init -n 
Writing .gitignore
Writing host.json
Writing local.settings.json
Created launch.json

下一步是通过执行 func new 来创建函数。我们将被要求选择要创建的函数类型和我们为函数选择的语言类型,然后将创建一个包含三个文件 function.jsonindex.jssample.dat 的函数文件夹。

$ func new
Select a language:
1\. C#
2\. JavaScript
Choose option: 2
JavaScript
Select a template:
1\. Blob trigger
2\. Cosmos DB trigger
3\. Event Grid trigger
4\. HTTP trigger
5\. Queue trigger
6\. SendGrid
7\. Service Bus Queue trigger
8\. Service Bus Topic trigger
9\. Timer trigger
Choose option: 4
HTTP trigger
Function name: [HttpTriggerJS] my-azure-func-use-case
Writing /Users/shashi/Documents/packt/chapter4/tutorial1/mylocalfunction/myLocalFunction/index.js
Writing /Users/shashi/Documents/packt/chapter4/tutorial1/mylocalfunction/myLocalFunction/sample.dat
Writing /Users/shashi/Documents/packt/chapter4/tutorial1/mylocalfunction/myLocalFunction/function.json

由于我们通过模板将函数包含进来,现在我们可以通过 func start 来本地调用该函数,此命令将在本地启动函数应用程序,并为我们提供 API URL:

$ func start
%%%%%%
 %%%%%%
 @ %%%%%% @
 @@ %%%%%% @@
 @@@ %%%%%%%%%%% @@@
 @@ %%%%%%%%%% @@
 @@ %%%% @@
 @@ %%% @@
 @@ %% @@
 %%
 %
[09/20/2018 20:57:09] Reading host configuration file '/Users/shashi/Documents/packt/chapter9/my-local-azure-function/host.json'
[09/20/2018 20:57:09] Host configuration file read:
[09/20/2018 20:57:09] {}
info: Worker.Node.769f18ed-34f3-4813-9e9f-b4d43ea01191[0]
 Start Process: node --inspect=5858 "/usr/local/lib/node_modules/azure-functions-core-tools/bin/workers/node/dist/src/nodejsWorker.js" --host 127.0.0.1 --port 65058 --workerId 769f18ed-34f3-4813-9e9f-b4d43ea01191 --requestId 964f6629-d61a-4006-a293-e0438da20e45
info: Worker.Node.769f18ed-34f3-4813-9e9f-b4d43ea01191[0]
 Debugger listening on ws://127.0.0.1:5858/0d0bfa98-d2a3-4af2-8c5c-3a2cc346b1fc
info: Worker.Node.769f18ed-34f3-4813-9e9f-b4d43ea01191[0]
 For help see https://nodejs.org/en/docs/inspector
[09/20/2018 20:57:10] Generating 1 job function(s)
[09/20/2018 20:57:10] Starting Host (HostId=shashismacbookpro-628939497, Version=2.0.11415.0, ProcessId=5072, Debug=False, ConsecutiveErrors=0, StartupCount=1, FunctionsExtensionVersion=)
[09/20/2018 20:57:10] Found the following functions:
[09/20/2018 20:57:10] Host.Functions.my-azure-func-use-case
[09/20/2018 20:57:10]
[09/20/2018 20:57:10] Job host started
info: Worker.Node.769f18ed-34f3-4813-9e9f-b4d43ea01191[0]
 Worker 769f18ed-34f3-4813-9e9f-b4d43ea01191 connecting on 127.0.0.1:65058
Listening on http://localhost:7071/
Hit CTRL-C to exit...
Http Functions:
my-azure-func-use-case: http://localhost:7071/api/my-azure-func-use-case

当从 http 调用该函数时,它的功能是通过 req 参数检索请求数据,并在请求体中查找 name 参数,成功执行后添加一些响应文本:

谷歌函数的附加组件

当我们决定迁移到云基础设施或决定使用 Google Functions 时,我们首先会进行概念验证或最小可行产品开发,以证明我们的应用程序在云环境中能够表现更好,并且我们不必担心基础设施问题。而且,如果在概念验证时功能的数量较少,手动开发、测试和部署它们会更容易,但随着功能的增多,管理开发、测试和部署会变得相当困难。将它们整合在一起,使其作为一个应用程序工作,还是一个挑战。因此,为了更顺利的开发,Google 还推出了 Google Cloud Functions 的 Node.js 模拟器。虽然目前它还处于 alpha 阶段,但它仍然是本地解决许多问题的好方法。

Google Functions 的 Node.js 模拟器作为 npm 标准包分发,因此必须通过 npm 命令进行安装:

$ npm install -g @google-cloud/functions-emulator

安装完成后,有许多命令可以用于设置本地环境以测试和调试 Google Functions。更多细节可以参考 cloud.google.com/functions/docs/emulator

在本章中,使用 Google Functions 的 DevOps,我们学习了如何使用 Google Cloud 的现成工具进行部署,但它们也可以在无服务器框架中进行部署,该框架也提供了可供使用的模板。

要使用无服务器框架,我们首先需要安装框架插件。插件的名称是 serverless-google-cloudfunctions。因此,如果我们需要快速开始使用 Google Functions,我们应该通过以下命令开始,它将创建一个简单的 helloworld Google Function,文件名为 indexserverless.ymlpackage.json。目前,服务器框架只支持 Node.js 运行时:

$ serverless create --template google-nodejs --path my-service-name

serverless.yml 中,我们需要配置 Google 项目名称和凭证。凭证 JSON 文件需要是绝对路径。但出于安全考虑,应该将其保存在服务器路径中的某个非常安全的位置:

provider:
    name: google
    runtime: nodejs
    project: my-serverless-function-usecase
    # the path to the credentials file needs to be absolute
    credentials: ~/.gcloud/Serverless-SIT.json
plugins:
    - serverless-google-cloudfunctions

然后通过 Serverless 命令将其部署到 Google Cloud。

$ serverless deploy -v
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Compiling function "first"...
Serverless: Uploading artifacts...
Serverless: Artifacts successfully uploaded...
Serverless: Updating deployment...
Serverless: Checking deployment update progress...
...............
Serverless: Done...
Service Information
service: my-g-function-usecase
project: serverless-sit-217120
stage: dev
region: us-central1
Deployed functions
first
 https://us-central1-serverless-sit-217120.cloudfunctions.net/http

一旦部署成功,我们应该在部署输出中获得 API 链接,并且可以在浏览器中访问它,屏幕应该显示:

你好,世界!

总结

在本章中,我们探讨了一些流行的无服务器使用案例,并介绍了一些能够改善开发和部署的基本要素。在下一章,也是最后一章中,我们将看到无服务器如何塑造 DevOps,以及 DevOps 在采用无服务器架构后必须做出的变化。

第十章:无服务器函数的 DevOps 趋势

最终,我们的旅程即将结束。在本书中,我们回顾了不同的无服务器提供商,以及多种构建、部署、监控和记录无服务器函数的方法,并且涉及了不同的服务提供商。那么,无服务器将如何改变 DevOps 的方向呢?正如我们所知,使用 IaaS 和 PaaS 时,我们需要构建基础设施,始终对其进行监控并进行扩展。我们需要使用配置管理工具不时地升级服务器,监控、记录并管理手动扩展,而当考虑到无服务器时,所有这些都不再需要。那么,当我们决定使用无服务器时,DevOps 将走向何方呢?曾经有一种看法认为如果我们采用无服务器,DevOps 就不再需要了,但这并不完全正确,正如我们通过各章所学到的,DevOps 并没有完全消失。我们仍然需要对代码进行版本控制、打包、部署,并进行监控和记录,但比 IaaS 和 PaaS 要更为简单。

本章将讨论两个小但非常重要的主题:

  • 无服务器对操作的影响

  • 无服务器的 DevOps 方向

无服务器对操作的影响

如我们所知,DevOps 实际上是开发人员和运维团队之间的合作,共同努力以更快地构建和生产应用程序。近年来,DevOps 变得非常强大,其价值随着加速和更好地支持应用程序部署而不断增长,并且它还促进了开发和选项过程的持续改进。

但开发人员其实并不太喜欢 Ops,因为他们总是不得不处理 Ops,以确保他们的代码在生产环境中按照业务需求正常运行和执行。操作的核心理念是提供基础设施,这在不同的环境(开发、QA、预生产和生产)中是相似的,从而让开发人员的工作更轻松,他们只需要专注于构建应用程序。

直到现在,操作员的工作一直是设置基础设施,最终通过脚本配置基础设施。操作员必须管理安全性、监控、记录以及许多其他日常工作,因此这需要大量工作,这也是推动我们向无服务器计算转型的原因。所以,开发人员重新专注于开发,创建新的功能和特性,减少了他们对代码是否能在提供的基础设施上运行,或是否会在代码中出现应用扩展问题的担忧。

基本上,无服务器架构从技术上来说是让开发者摆脱了运维的负担,这样开发团队和运维团队可以更加专注于自己的任务,而不必浪费时间处理基础设施问题。那么,无服务器架构是如何产生的呢?无服务器架构的诞生有很多因素和原因,其中一个关键因素就是容器的普及。容器早就存在,但随着 Docker 和 Kubernetes 的出现,容器变得更加流行。从概念上讲,无服务器架构基本上是将微服务拆分成更小的部分,这些部分可以被称为纳米服务(nanoservices)。这些纳米服务能够完美适应容器,从而实现纳米级的功能。同时,我们也知道,借助 Kubernetes 等服务,容器的管理变得非常简便。这可能是无服务器架构受欢迎的原因之一。

随着云服务提供商的引入,基础设施变得更加稳定且成本更低,但对于虚拟机,我们仍然需要进行配置和管理。然而,随着云服务商引入无服务器架构,运维团队不再需要担心配置和管理。实际上,运维团队的角色将逐渐消失,但并非完全消失。他们将不再负责基础设施的配置和管理,而是能全身心投入到监控、日志管理、服务部署和管理的编排、安全管理等方面。虽然虚拟机/容器的管理已不再是重点,但性能监控、安全监控等任务仍然存在,简而言之,运维团队需要提升技能,以适应云服务的需求。

DevOps 在无服务器架构中的发展方向

当没有 DevOps 时,开发和运维是各自独立工作的,这导致了团队协作不畅和缺乏透明度。运维流程极其缓慢,且无法控制环境的整合。随着 DevOps 团队的整合,所有成员统一协作,共同参与应用生命周期的管理,通过技术栈和工具实现自动化。DevOps 成为组织的核心支柱,也成为市场响应速度的重要组成部分。

然而,DevOps 在实施过程中也面临自身的挑战。首先,DevOps 是一个庞大的专业领域,很难找到在每个方面都具备专长的专家。因此,组织在实施 DevOps 时常感到困难,也容易导致实施过程中的困惑。

DevOps 的工具选择是另一个独立的挑战。持续部署、持续测试和协作报告是 DevOps 成功的关键。然而,许多组织在工具选择上犯错,错误的工具选择最终会导致失败。

很多管理者对 DevOps 能否成功表示怀疑。他们担心 DevOps 是否能够为客户和组织提供所需的透明度。

公司越大,协作越困难,因为员工各不相同,团队也相对独立,这就造成了壁垒。但使团队理解如何打破这些壁垒、促进协作是至关重要的。

DevOps 团队主要花时间为开发团队提供支持,包括构建、设置和维护开发环境,以及解决部署问题,但他们最终会更多关注运营方面的工作。

但无服务器计算帮助克服了这些挑战。它为运营团队腾出了空间,使其能够更有效地分配时间,并为现有生态系统添加改进。无服务器计算使得商业敏捷性成为可能,因为你可以创建一个持续改进开发的环境。

不再需要担心基础设施,DevOps 团队可以在开发的其他方面进行改进,比如自动化部署、添加代码版本管理流程、自动化部署度量、为应用程序添加更多价值(例如引入不同的仪表盘),以及应用程序性能管理。

由于无服务器计算仍处于成长阶段,在语言或运行时支持、供应商锁定、安全性方面仍有许多待解决的问题,并且目前并非所有应用程序都支持无服务器架构。此外,市场上还涌现了许多开源无服务器架构,如 OpenWhisk、OpenFaaS、FN、Kubeless 和 Iron.io (www.iron.io/)。在无服务器计算及其周围的 DevOps 工具领域,成熟的道路仍然漫长。

由于安全原因以及云技术 adoption 缓慢,很多银行客户和政府组织对云服务持谨慎态度。但像 Kubernetes 这样的技术正逐渐获得大量关注,它的性能、功能以及相关论坛的支持随着时间的推移不断提升。因此,政府组织和银行领域的客户可能会更多地倾向于采用 Kubernetes,因为微服务与 Kubernetes 可以结合起来,作为无服务器功能进行使用。所以 DevOps 在这里肯定会发挥重要作用。

因此,可能需要在本地部署 Kubernetes 等技术,配置和设置服务器,升级基础设施,进行监控和日志记录,并根据需要增加基础设施以支持扩展。

但是,如果某些公司选择使用云服务提供商来满足其无服务器需求,那么 DevOps 的工作轨迹将发生变化。在开发方面,云操作工程师和开发人员的角色将融合为一体,因为将不再需要处理底层基础设施。但我们仍然需要持续集成、持续部署、监控和日志记录等服务。

DevOps 将在部署管道、自动化指标通知、自动化日志记录和监控的设置中非常重要。但当一切都被自动化并流线化后,DevOps 的工作量将会减少,这对于任何类型的基础设施来说最终都是一样的。

但工作职责肯定会发生变化,许多角色也将改变。例如,我们将需要更少的网络和基础设施工程师。因此,在迁移到无服务器架构之前,建议我们评估 IT 运维。如果不这样做,采用无服务器架构可能会拖慢进程,甚至成为运营的瓶颈。

所以,总结来说,由于无服务器架构仍在不断完善,我们很难预测 DevOps 在无服务器采纳方面将如何发挥作用。方向可能会改变,但旅程仍将继续。

总结

这本书的旅程已经结束,但这只是对本书而言。就无服务器而言,这个旅程才刚刚开始,随着这个成熟过程的推进,我们将看到在服务、技术以及新供应商方面的许多改进。与此同时,我们还将看到无服务器应用程序的 DevOps 工具和流程的成熟。因此,在这里,我们结束了本书,期待无服务器和 DevOps 与无服务器一同成长。

posted @ 2025-06-27 17:08  绝不原创的飞龙  阅读(66)  评论(0)    收藏  举报