谷歌云无服务器计算实用指南-全-

谷歌云无服务器计算实用指南(全)

原文:annas-archive.org/md5/b5053dbe7c17aa6a92370ac01eedf297

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

无服务器世界及其为新一代开发者带来的巨大潜力,已经引起了广泛的关注。在 Google Cloud 实战:无服务器计算 一书中,我们聚焦于 Google Cloud 提供的三大无服务器平台:App Engine、Cloud Functions 和 Cloud Run。每一节将为您提供产品的高层次介绍,并概述每项技术的主要特点。书的结尾部分将通过两个案例研究,展示无服务器的强大功能。这些案例研究的目的是演示如何在 Google Cloud 上管理无服务器工作负载。

本书适合的人群

本书的受众是那些正在使用 Google Cloud 并希望了解如何将无服务器技术与项目集成的人员。书中内容涵盖了 Google Cloud 上支持无服务器工作负载的主要产品。此外,书中还提供了多个示例,旨在帮助开发者自信地在 Google Cloud 上构建无服务器应用。

本书内容

第一章,介绍 App Engine,提供了 App Engine 的高层次概览,App Engine 是 Google 在 2008 年发布的首个无服务器平台(没错,已经十多年了)。本章让你了解什么是 App Engine,接着再深入探讨支持组件的细节。

第二章,使用 App Engine 开发,让你全面了解 App Engine 的各种功能。在本章中,我们阐述了平台的关键要素,展示了即便是最基础的应用也能轻松实现规模化和弹性扩展,而无需额外的维护工作。

第三章,介绍轻量级函数,讨论了转向单一功能函数的价值。减少应用程序的体积有助于更容易地调试和应对工作负载。在本章中,我们还介绍了 Google Cloud Functions 的概念,它将这一理念应用于在 Google Cloud 上提供无服务器工作负载。

第四章,开发云函数,介绍了如何在 Google Cloud 上使用云函数。在本章中,我们重点讨论了如何构建、部署和维护无服务器应用的关键要素。为了补充本章内容,我们讨论了功能框架(Functions Framework),它支持本地开发模拟云环境,并介绍了新手网页开发者需要了解的 HTTP 协议。

第五章,探索作为服务的函数,通过一系列示例构建一个服务。在本章中,我们开始利用 Google APIs 和各种资源与云函数进行集成。通过本章内容,我们可以看到事件处理如何帮助简化代码的开发。

第六章,Cloud Functions 实验室,创建了一系列示例,展示 Cloud Functions 的实际用例。在这一章中,我们将演示如何构建响应事件的 Web 组件。我们还将探讨如何通过服务账户保护 Cloud Functions。

第七章,介绍 Cloud Run,将讨论转向一个新主题——Cloud Run。在这一章中,我们将介绍与微服务和容器工作相关的一些关键基础知识。此外,我们还将介绍 Knative、gVisor 及其与 Cloud Run 的关系。

第八章,使用 Cloud Run 进行开发,讨论了 Google Cloud 上 Cloud Run 的界面以及如何使用它构建复杂的系统,如 REST API。我们还首次探讨了如何通过自动化一些任务来提高开发人员的生产力,使用 Google Cloud 的开发工具集。

第九章,为 Anthos 开发 Cloud Run,介绍了 Cloud Run for Anthos 的概念,这意味着我们第一次提到了 Kubernetes。这一章对于理解未来平台以及 Cloud Run 如何为多个环境提供支持至关重要。

第十章,Cloud Run 实验室,讲解了如何使用 Cloud Run,以及如何在 Google Kubernetes Engine 上集成无服务器工作负载。此外,我们还探讨了如何与 Cloud Run 产品一起进行持续集成(CI)。

第十一章,构建 PDF 转换服务,介绍了 Cloud Run 在 Google Cloud 上的两个用例中的第一个。在这个示例中,我们学习如何集成一个开源应用并将其部署为容器。此外,我们利用 Google Cloud Storage 的事件通知来最小化额外代码的开发。

第十二章,通过 REST API 使用第三方数据,继续探索 Google Cloud 上的 Cloud Run。在第二个示例中,我们学习如何为概念验证应用构建多个服务。该应用演示了几种技术,并展示了如何集成服务,如 Cloud Pub/Sub、Cloud Build 和 Container Registry。在练习结束时,你将获得如何消费 JSON 并围绕数据管理和传播构建可扩展解决方案的经验。

为了最大限度地利用本书

如果你已经具备如何在 Google Cloud 中进行导航的基本理解,包括如何访问所提供产品的控制面板,以及如何打开 Cloud Shell,那将是最理想的。

本书的大多数活动需要一个 Google Cloud 项目,另外,你也可以使用像 Qwiklabs(qwiklabs.com)这样的沙箱环境。使用沙箱可以确保你所做的任何更改不会影响你常规的 Google Cloud 项目。

大多数章节都包含示例代码,可以通过以下链接获取:

github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud-Platform

本仓库包含每个章节所需的基础组件以及一个解决方案子目录。

完成每章末尾的所有小测验,并在进入下一章之前解决任何错误的答案。你必须了解为什么某个选项是正确答案,而不仅仅是知道它是正确答案。

这本书分为四个部分。为了全面了解特定的产品,我建议阅读关于App Engine(第 1-2 章)、Cloud Functions(第 3-6 章)和Cloud Run(第 7-10 章)的章节。若要查看如何在 Google Cloud 上部署无服务器工作负载的实际示例,请参阅第十一章和第十二章中的示例。

下载示例代码文件

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

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

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

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

  3. 点击“代码下载”。

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

一旦文件下载完成,请确保使用以下最新版本解压或提取文件夹:

  • WinRAR/7-Zip(适用于 Windows)

  • Zipeg/iZip/UnRarX(适用于 Mac)

  • 7-Zip/PeaZip(适用于 Linux)

本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud。如果代码有更新,它将会在现有的 GitHub 仓库中更新。

我们还提供了其他代码包,来自我们丰富的书籍和视频目录,访问链接是github.com/PacktPublishing/。快来看看吧!

下载彩色图像

我们还提供了一份包含本书中截图/图表彩色图像的 PDF 文件。你可以在这里下载:static.packt-cdn.com/downloads/9781838827991_ColorImages.pdf

使用的约定

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

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。示例:“在命令提示符中,键入 hostname 并按下 Enter 键。”

粗体:表示新术语、重要词汇或屏幕上显示的词语。例如,菜单或对话框中的词语在文本中会这样显示。示例:“从上下文菜单中选择属性。”

警告或重要提示将以这种方式显示。

小贴士和窍门将以这种方式显示。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提及书名,并通过 customercare@packtpub.com 联系我们。

勘误:尽管我们已尽力确保内容的准确性,但错误难免。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接,并输入相关信息。

盗版:如果您在互联网上遇到我们作品的任何非法复制,我们将非常感激您提供该材料的地址或网站名称。请通过 copyright@packt.com 联系我们,并附上该材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专长,并且有兴趣撰写或参与编写书籍,请访问 authors.packtpub.com

评论

请留下评论。当您阅读并使用本书后,为什么不在您购买的站点上留下评论呢?潜在的读者可以看到并根据您的公正意见做出购买决策,我们在 Packt 可以了解您的想法,我们的作者也能看到您对他们书籍的反馈。谢谢!

若要获取有关 Packt 的更多信息,请访问 packt.com

第一部分:App Engine

在本节中,您将了解 App Engine 以及使其成为可扩展 web 应用程序的强大产品的特性。初步讨论将介绍该产品,并突出其主要特点,例如 App Engine Standard 与 App Engine Flex 之间的差异、任务队列执行的操作,以及 App Engine 如何无缝地处理应用版本管理。

本部分包括以下章节:

  • 第一章,介绍 App Engine

  • 第二章,使用 App Engine 开发

第一章:介绍 App Engine

在本章的第一部分,我们将讨论Google App EngineGAE)的主要特性及其平台即服务PaaS)方法。自 2008 年起,GAE 提供了一个无服务器环境,用于部署基于 HTTP/网页的应用程序。

在本章中,我们将探索 GAE 的框架和结构,了解如何在此平台上实现高可扩展的应用程序。作为其中的一部分,我们将考虑如何在 GAE 上集成标准的网页基本功能,如流量分配和 API 管理。到本章结束时,你应该已经掌握了使用 GAE 快速构建基于网页的应用程序的坚实基础。

简而言之,本章将涵盖以下主题:

  • 介绍 GAE

  • 了解 GAE 框架

  • 定义 App Engine 组件

  • 了解 GAE 的功能

介绍 GAE

在软件工程创新方面,显然 Google 拥有丰富的历史。这一创新贯穿于多个成功的项目,包括多个拥有数十亿用户的产品,如 Google 搜索、Android 和 YouTube。Google Cloud 及其充满活力的服务生态系统提供了为这些关键项目打造的工具,现在你也可以将你的应用托管在同一个平台上。

GAE 旨在托管基于网页的应用程序,并优雅地处理请求/响应通信。了解如何在 Google Cloud 上实现这一点,对于构建一致且高效的应用程序至关重要,这样可以通过其响应性让最终用户感到满意。

在深入探讨 GAE 的细节之前,我们将花一些时间讨论应用平台背后的基本原理。在接下来的段落中,我们将概述 GAE 的主要元素,为我们提供足够的知识,以便做出明智的决策,了解哪些类型的应用程序适合在 GAE 上运行,哪些则不适合。

为了开始我们的旅程,我们将通过回答以下问题来建立对 GAE 应用平台提供的共享理解:

  • 为什么选择 App Engine 的无服务器架构?

  • App Engine 的底层框架是什么?

  • App Engine 如何处理自动扩展?

  • 目标受众是谁?

为什么选择 App Engine 的无服务器架构?

在互联网上提供服务需要仔细考虑,以最大限度减少系统被攻击的潜在风险和相关的安全隐患。所有指向 App Engine 的应用程序流量都通过Google Front EndGFE)服务进行传递,以减少访问协议遭到破坏的风险。

GFE 为所有 GAE 注册的路由 Web 流量提供 传输层安全TLS)终止。作为保护层,GFE 能够为 Google Cloud 项目执行多个基本的安全服务。从安全角度来看,它提供了公共 DNS 名称的公共 IP 托管和 拒绝服务DoS)保护。此外,GFE 还可以作为可扩展的反向代理供内部服务使用。

在 Google Cloud 中,常提到的一个术语是 深度安全。这种方法提供了多个并行的保障措施,以应对那些试图滥用你服务的恶意行为者。这些安全措施中的许多已经内置于平台中,因此开发者无需额外努力。

GAE 提供了一个完全托管的应用平台,使开发人员只需专注于构建他们的应用程序。对于计算和存储等低层次基础设施的管理问题,服务会自动处理。在这一点上,像 GAE 这样的无服务器解决方案提供了将焦点集中在开发过程上的能力,并将运营事务交给服务提供商处理。

GAE 使开发人员能够利用一个简化的无服务器环境,在 Google Cloud 上托管 Web 应用程序和 API 服务。通过提供一个显著简化的环境,目的是通过吸引更多开发人员进入云端来增加云平台的采用率。在大多数情况下,当开发人员使用这样的系统时,他们可以立即看到在这样的环境中工作所能带来的高效潜力。

在下面的示意图中,我们概述了基于 GAE 的环境的典型工作流程的逻辑视图。从图示中我们可以看到,所有外部通信都使用 HTTP(S) 协议进行,并通过 Cloud Load Balancer(由 GFE 提供)进行路由。在这种场景下,前端设备暴露一个单一的服务名称,封装了部署的应用资源。该服务使得 GAE 可以动态地将接收到的流量引导到多个后端资源组件。GAE 负责确定这些组件执行的角色,并确保每个组件在标识上保持独立:

后端服务通信使用 HTTP/HTTPS 协议,这意味着 GAE 假设一个基于互联网的环境(即,假设你可以访问一个面向公众的互联网连接)。应用请求处理由部署的默认实例执行,该实例会根据系统工作负载阈值进行自动扩展。

采取上述方法能够使工作负载在应用实例之间无缝地进行负载均衡,同样无需开发人员进行任何额外的配置。标准的工作负载操作活动,如 TLS 终止和 DNS 解析,也无需进一步的用户配置。这些活动的加入为开发者提供了显著的好处。由于应用工作负载受到独立实例的约束,意味着应用也能够实现巨大的规模,而无需任何实质性的工作。

除了标准保护外,GFE 的加入还提供了与安全交付协议(如 gRPC)无缝兼容的功能(grpc.io/blog/principles/)。gRPC 协议使用 RPC 框架在转发服务请求时提供层隔离。此外,通信默认保持加密,以避免在进行服务间通信时被窃听或设备被攻击。

最近,行业的采用使得 gRPC 得到了更广泛的应用,且在多个服务间的兼容性得到了更广泛的拓展。例如,RPC 安全协议在 Google 被广泛使用,用于保护 API 访问。在跨互联网使用通信协议时,有许多标准存在。通过将所有与服务相关的流量路由通过 GFE,意味着可以提供一个极其灵活和可扩展的前端,而无需额外的工作。

App Engine 有两个版本:

  • App Engine Standard

  • App Engine Flex

两个版本有许多共同点,本章中大多数内容都适用于这两个版本。然而,在考虑这两种环境时,有一些关键属性需要特别指出,以下图表中将重点展示:

在上述图表中需要特别指出的一点是,App Engine Standard 可以缩放至零。然而,App Engine Flex 环境的最小缩放值为一个实例。因此,如果您的主要考虑是成本,使用 App Engine Standard。能够缩放至零相较于 App Engine Flex 环境提供了真正的优势,后者始终会有一定的成本。

GAE Standard 能够缩放至零是由于使用了沙箱环境。使用专用沙箱提供了更快的响应速度,即更快的启动时间和自动扩展响应。以秒为单位测量部署时间,也可能是考虑到不同应用增长模式所需的灵活性时的一个优势。

与标准环境不同,GAE Flex 使用 Google Compute Engine (GCE),更具体地说是 托管实例组 (MIGs),来实现自动扩展。在这个环境中,GAE Flex 始终有一个计算实例的开销。由此产生的费用还需要考虑 GAE Flex 所需的计算资源。维持在此环境中的应用程序也意味着初始化时间更长(即冷启动),因为需要启动一个 GCE 实例以及为任何基于灵活部署的应用程序提供容器环境。

在应用环境中还存在其他差异。然而,前述特性是当开始在 GAE 上构建应用时,通常影响决策的因素。

目标受众是谁?

在 GAE 完全托管的无服务器应用平台上工作,消除了与构建互联网规模应用相关的许多历史性限制。使用这种新范式,开发人员可以专注于构建复杂的 Web 应用和 API,而无需了解后端服务和低级别的网络或基础设施。

构建无服务器应用程序意味着敏捷的代码可以快速部署到云端。Web 应用(例如,参见以下列表)无疑是这种解决方案的最佳应用领域:

  • Web 应用

  • 移动后端

  • HTTP API

  • 业务应用程序 (LOB) 应用

如果这听起来是你的工作负载可以从中受益的领域,那么你就是目标受众。在一个无需关心创建或维护基础设施的环境中工作,对于大多数开发人员来说是非常理想的。GAE 正是基于这一前提构建的,并为开发人员提供了出色的开发和部署体验,无需关注底层技术。

在概述了为 App Engine 提供的不同环境后,我们可以开始探索是什么让这个产品如此迷人。

理解 App Engine 框架

探索 App Engine 的一般架构揭示了其底层框架已经实现了多少,旨在为 Web 应用开发提供集成的工作流。

Google 将许多内部服务捆绑在一起,以最大限度地减少开发人员将其应用程序转为云原生应用所需的努力。再加上 GAE 服务固有的自动扩展能力,服务创建者无需采取额外措施即可实现这一点。

在这个平台上创建一个 Web 应用程序可以像将代码部署到 App Engine 环境一样简单。然而,在幕后,实际发生了许多活动,以确保应用程序能够成功部署、基础设施得到配置,并且整个系统最终能够智能地扩展。接下来,我们通过下面的图示,展示了 App Engine 框架中的底层工作原理,并介绍了支持 App Engine 的可选组件:

从更广泛的视角审视 GAE,可以看出有许多高级组件被用来构建完全托管的应用平台。当然,作为一个无服务器环境,你实际上不需要了解幕后发生了什么。

对任何平台的概念性理解在开发过程中是非常有用的。无论服务如何试图将信息从你那里抽象出来,若你理解各种组件如何集成,解决技术问题就会变得无比容易。

除了标准环境外,GAE Flex 还支持自定义容器运行时环境。自定义容器部署在 GCE 上,使开发者能够构建自己的环境。通过这样做,突然间可以获得更高水平的定制化,极大地拓宽了 GAE 吸引更广泛受众的能力。容器的普及使得引入 GAE Flex 环境成为一个具有吸引力的选项,特别是在需要更多控制时。

然而,相比标准环境,使用 GAE Flex 也有一些性能和成本的影响。理解这些限制对于应用开发者至关重要,这些内容在 GAE 的规范中有明确说明。清楚了解设计考量中的各种优缺点,将帮助解决任何疑虑,并使选择最合适的环境变得更容易。有关详细的差异,请参阅本章的 支持的运行时语言 部分。

除了前面提到的 GAE 环境之外,还有一些其他重要的组成组件在后台工作。服务层增强了 GAE 的计算功能,并提供了存储、队列、缓存以及与 Google Cloud API 进行身份验证的通信能力。

App Engine 组件

在接下来的几个小节中,我们将探讨与这些服务层组件相关的主要内容。

任务队列

通过使用额外的解耦算法来管理信息流,系统保持响应性。GAE 使用消息队列来保持 Web 流量的亚秒级响应率。长时间处理的任务会被移交给任务队列系统,以释放请求/响应周期。

在更细粒度的层面上,任务队列采用两种方法来管理与网页请求/响应周期相关的异步信息处理:

服务调度 描述
推送队列(HTTP 请求) 调度请求。保证任务执行。用例:将短期任务排队,这些任务可以在一段时间内完成,或在涉及特定时间动作的情况下执行,类似于定时事件任务执行。
拉取队列(请求处理器) 租赁机制。提供比调度请求更多的灵活性。为任务提供生命周期。用例:批处理,可以一次性完成,而无需逐项处理信息。

任务队列提供了一种与网页流量事务隔离的调度机制。在此服务中,我们将与网页请求相关的信息处理部分隔离,以最小化请求与响应之间的完成时间。添加任务队列使 HTTP 请求/响应周期能够保持高效。

Memcache

GAE 环境的一个重要特性是包含了 memcache。Memcache 被抽象为持久化存储的外部缓冲区,以提供快速的数据访问。为应用添加一个低延迟的数据层,为重复的访问请求建立一致的机制。Memcache 提供了一个基于内存驻留(in-memory)的临时数据存储的便捷数据访问层。

为服务层定义了两个级别的 memcache 服务:

  • 共享 memcache:这是 GAE 的默认设置。共享 memcache 提供了默认的访问模式。在大多数情况下,无需更改应用使用的缓存级别,因为默认设置已经足够满足大多数工作需求。

  • 专用 memcache:这是一个高级设置,用于保留专用的应用内存池。

专用 memcache 服务为应用提供了更大的效率范围。作为一种快速数据检索机制,缓存用于访问临时数据,如果数据访问是应用的核心,那么探索此选项可能会非常有用。

请注意,后一种选项是付费服务,不同于默认的缓存设置。然而,这个选项可以保证为可能需要高频数据访问的应用保留更大的内存空间。

当处理一个主要以读取模式为主的应用时,对于待消费的数据,保持 memcache 和数据存储同步是有益的。读取模式可能是大多数 GAE 开发者遇到的最常见用例,对于这种场景,GAE 完全能够满足大多数应用需求。

存在更复杂的使用场景,例如需要同时进行读写同步的数据库模式。在缓存层和后端数据库之间,需要考虑如何管理缓存层和 Datastore 的集成。在与 Datastore 交互为优先事项的情况下,Cloud NDB 缓存提供了一种适用于更高级需求的配置。在这种使用场景中,进行一定的调查以优化数据管理和数据刷新将是有益的。在这种情况下,底层系统只能提供有限的优化,进一步提高效率将需要在迭代应用开发生命周期中进行额外的设计。

数据存储

GAE 提供了多种数据存储选项,包括无模式和关系型数据库存储。后端数据存储(如 Datastore/Firestore 或 Cloud SQL)使开发者能够在广泛的用例中提供一致的访问,并与 GAE 无缝集成。

以下表格提供了无模式和关系型数据库之间映射的高层次概述:

Cloud SQL(关系型) Cloud Datastore(无模式)
种类
实体
属性
主键

App Engine 提供多种选项,赋予开发者根据应用程序的需求使用后端存储的能力。在大多数情况下,考虑如何在选定的 Datastore 中存储信息也是至关重要的。与任何开发一样,理解底层数据及其访问方式也非常关键。

Cloud Datastore

Cloud Datastore 将是任何 GAE 开发的标准组件。与其他应用平台类似,开发初期几乎不需要理解数据库管理。作为一种托管的无模式(NoSQL)文档数据库,Datastore 在大多数情况下已经足够使用。

以下高层次要点最为关键,适用于将 Datastore 与 GAE 一起使用:

  • Datastore 是一个 NoSQL 无模式数据库。

  • App Engine API 访问。

  • 设计用于自动扩展到大规模数据集(即低延迟的读取/写入)。

  • 存储有关请求处理的信息。

  • 所有查询均由先前构建的索引提供服务。

作为 App Engine 的核心组件,Cloud Datastore 适用于高性能、应用开发和自动扩展。一旦 Datastore 初始化完成,就可以开始使用数据。处理存储在 Cloud Datastore 中的数据非常简单,因为不需要提前将数据附加到后端。如果你来自关系型数据库背景,这种方式可能会令人不适应。

在创建数据库时,值得考虑如何为信息建立索引,以确保无论使用情况如何,访问性能始终保持良好。有许多很好的参考资料可以帮助建立适合访问数据的机制,例如如何创建基本索引和复合索引。熟悉这些内容将为你提供持续的帮助,特别是在出现问题时,例如在 GAE 上托管的应用出现性能延迟时。

必须考虑如何存储 Datastore 中的信息。如果你的 Datastore 不是应用程序的核心,创建以数据为中心的应用时,数据管理问题将不再相关。由于数据布局效率低下导致的 Datastore 性能下降,需要考虑如何设计数据表示,以便在后期节省大量重构工作。

目前,了解无模式数据库非常适合大多数 App Engine 的需求,并且 Cloud Datastore 是一种文档数据库,这为它们的使用场景提供了极大的线索。超越存储文档数据的初步条件(例如,实体和种类)时,仔细考虑合适的访问方法,将随着应用复杂性的增加而带来更多的好处。

Cloud SQL

在使用 Cloud SQL 时,Google Cloud 当前提供两种产品,即 MySQL 和 Postgres。这两种选项都提供了与 GAE 配合使用的托管关系型数据库。需要澄清的是,托管在此语境下意味着服务提供商负责备份和更新的维护,而无需用户的干预。

Cloud SQL 提供了支持事务的关系模型。如果你的应用部署在 App Engine 上,且需要关系型数据库,那么可以考虑使用 Cloud SQL。处理多个数据库类型可能会令人困惑,因此,在开始开发活动之前,务必清楚选定的 Datastore 应该如何使用。一个关键优先事项是确保设计能够代表应用如何使用信息。

尝试将 Datastore 用作 在线事务处理 (OLTP) 后端是一个不必要的任务。同样,尝试在没有相关模式或规范化的情况下使用无模式数据存储在 Cloud SQL 数据库中,也无法获得最佳性能。

虽然花时间定义要应用的模式规范化是至关重要的,但这一要求可能会随着时间的推移而发生变化。处理数据从来不是简单地上传内容然后忘记它,因此在应用开发生命周期中,特别要关注这一部分,以便获得最大的效益。

处理 App Engine 上的自动扩展

在本节中,我们将讨论 App Engine 如何处理自动扩展。在大多数情况下,GAE 会通过其分布式架构处理任何工作负载。当然,如果你有更高级的需求,那么了解 GAE 如何执行实例自动扩展是值得的。

在 GAE 中,实例扩展的定义包含在配置文件中。以下两个配置项尤其相关,并在此进行了概述(即扩展类型和实例类别):

扩展类型 实例类别 描述
手动 固定 提供多个预配置的实例。修改实例数量需要系统管理员的手动干预。
自动扩展 动态 根据从系统收集的遥测数据(例如响应延迟和请求率),自动扩展决定是否增加/减少实例数量。

在考虑自动扩展服务时,必须考虑如何设计应用程序以充分利用各组成部分。以下是构建可扩展解决方案时需要考虑的一些因素:

  • 负载测试对于建立应用程序的最佳性能设计至关重要。在大多数情况下,使用真实世界的流量进行测试提供了检测系统瓶颈的最佳场景。

  • Google Cloud 对所有项目实施配额限制,因此在创建应用程序时要注意这一点。配额限制适用于 API 调用和计算资源。

  • 在大多数情况下,一个任务队列就足够了。然而,GAE 确实提供了在长时间运行的任务需要更高处理吞吐量时对任务队列进行分片的功能。

在下一部分,我们将从 App Engine 的整体架构转向讨论实现的具体细节。在这部分讨论中,将概述支持的语言。

定义 App Engine 组件

本节的目的是描述 GAE 的详细信息。GAE 应用平台的性质是提供一个无服务器的应用环境,能够支持多种语言的运行时。运行时支持要求存在两个版本的 App Engine。两个环境之间的关键区别与所支持的语言运行时相关。

支持的运行时语言

历史上,GAE 运行时仅支持有限数量的语言,但随着时间的推移,已扩展到提供更广泛的语言支持。语言限制曾是 GAE 初版发布时最常见的批评之一。这一情况在过去几年中有了显著改善,现在支持更多的运行时环境,包括以下内容:

  • Python 2.7/3.7

  • Java, Node.js 8/10

  • PHP 5.5/7.2

  • Go 1.9/1.11/1.12

App Engine – 标准

在此环境中,沙盒包装提供了应用程序隔离并限制对特定外部资源的访问。根据选择的运行时,安全措施强制执行沙盒环境,例如应用访问控制列表,以及替换语言库。

在下图中,GAE Standard 使用沙盒环境,支持在单一可用区内运行 0-N 实例:

当使用需要运行时语言的应用程序时,如 Python、Java、Node.js 或 Go,GAE Standard 是最佳选择。GAE Standard 在沙盒环境中运行,确保实例可以缩减至零。缩减到零意味着在这种情况下的成本非常低。

App Engine – 灵活环境

在 GAE Flex 环境中,容器驻留在 GCE 实例上。虽然在某些方面,这与 GAE Standard 提供了相同的服务访问,但从沙盒迁移到 GCE 存在一些缺点,特别是实例的预热速度和成本。

在下图中,GAE Flex 使用容器环境来创建资源,支持在区域 MIG 中运行 1-N 实例:

居住在 GCE 上的容器基于 Docker 镜像,提供了一种替代前述沙盒环境的方式。使用 GAE Flex 需要在速度和成本上做出一定的妥协。速度的牺牲源于需要通过 Cloud Build 过程启动容器以将代码部署到 GAE 环境中。由于至少需要保持一个实例处于活动状态,这意味着这种情况下始终会产生一定的成本。

尽管 GAE 隐藏了构建过程,但与沙盒方法相比,构建自定义运行时所需的时间并不容忽视。完成构建过程后,镜像会被发布到 Google Cloud Build,并准备在应用程序内部署。那么,GAE 的主要特点是什么呢?接下来的章节将介绍 GAE 的主要属性。

了解 App Engine 特性

在接下来的几个小节中,我们将描述 GAE 的一些关键方面,从应用程序版本管理开始。

应用程序版本管理

GAE 使用配置文件来管理要部署的应用程序版本。例如,针对 Python 部署的运行时,app.yaml 配置文件包含与应用程序相关联的版本 ID。除了版本之外,应用程序默认还指定了系统的主实例。

每个部署的应用版本将保持其独特的 URL,开发者可以同时查看多个版本。同样,发布版本可以根据所选的部署方案进行升级或回滚。当新的代码部署时,会为配置代码应用一个独特的索引,以确保每个应用修订版能够成功区分已部署的旧版本和新版本。

在下图中,三个版本的应用程序已经在 GAE 上部署。然而,除非应用管理员另有说明,否则流量只会路由到默认版本:

GAE 采用的方式意味着部署管理非常简便,系统管理员可以一键更新。类似地,他们也可以通过控制台执行更复杂的部署,而不失去对应用程序先前修订版的访问。

除了内置工具外,App Engine 还支持源代码版本控制。使用版本控制中存储的代码与使用本地文件的方式非常相似。系统代码访问的决策由开发者决定;例如,他们可能选择使用 Cloud Source Repositories 来部署代码。

如果你不熟悉 Google Cloud Source Repositories,它本质上是与项目环境直接关联的 Git 仓库。如果你熟悉 Git,那么你将能够迅速使用 Google Source Repositories 启动工作。从这里开始,你完全可以从外部源(如 Bitbucket、GitLab 或 GitHub)镜像代码。

对于更简单的使用场景,从本地文件部署可能足以满足大多数实例的需求。然而,采用更一致的方法有助于管理项目中的代码。一旦应用成功部署,接下来的步骤是决定如何管理流量流向这个新部署的应用。

流量分配

流量分配提供了一种有效的方式来在版本之间切换。GAE 提供了几种选项来简化这一过程。此外,你不需要跟踪当前已部署的应用版本。

GAE 流量分配可用的选项如下:

  • IP 流量:使用源 IP 来确定从哪个实例提供响应

  • Cookie 分配:根据名为 GOOGAPPUID 且值在 0-999 之间的 Cookie 来应用会话亲和性

  • 随机:使用随机算法根据前述选项来提供内容

在下图中,流量路由到 GAE 后会被分配到两个实例。在这种情况下,基于 IP 地址可以明显看出 50% 的流量分配:

部署命令确保了一个简单的过程,使得从一个版本切换到另一个版本成为可能。作为一个完全托管的应用平台,App Engine 的责任是简化如何根据定义的流量拆分偏好来部署应用。

在进行新部署的情况下,可以告诉 App Engine 应该将多少流量发送到更新后的应用部署。例如,这种技术在进行 A/B 测试时非常有用,可以对比两个不同版本的部署。使用此功能可以在部署新版本时,开启许多不同的部署和测试方法。如果实现的代码出现问题,有许多工具可用来帮助调查。一个这样的工具是 Stackdriver,我们将在下一节更详细地了解这个产品。

监控、日志记录和诊断

Stackdriver 是 Google Cloud 的默认监控解决方案。在观察与 GAE 相关的信息时,可以使用 Stackdriver 中的资源类型 gae_app 来过滤专门与环境数据相关的信息。在下面的图示中,我们可以看到项目的流量由 GCE 管理,GCE 负责与其他服务的连接,例如 任务队列MemcacheStackdriver

Stackdriver 日志记录作为标准功能适用于任何 GAE 环境,提供了实时查看应用程序中正在执行的操作的能力。通过应用程序生成的日志可以根据需要进行查询。这个日志记录过程不需要额外的配置,并且适用于所有可用的与应用程序相关的日志。供参考,当使用 GAE 中的记录时,必须注意 Stackdriver 中可用的以下数据源:

  • 请求日志:这些提供了与对应用程序发出的请求相关的默认信息。此日志的资源名为 request_log。你可以在 Stackdriver 日志查看器中的 appengine.googleapis.com/request_log 过滤器下查看。

  • 应用日志:这些提供了补充请求日志的额外应用信息。

  • 第三方日志:这些是特定于应用的,除此之外还有前述的日志。可能会有特定包的信息被发送到系统日志。如果存在此类信息,可以通过 API 在日志查看器中访问相应的条目。

Stackdriver Trace 同样不需要额外的工具来与 App Engine 一起工作。使用这个解决方案时,自动启用应用程序跟踪数据的监控。数据会被集成到默认的 GAE 设置中,并可以在 Stackdriver 环境中访问。

在使用 App Engine Flex 环境时,日志通过 Google Cloud 客户端库或 stdout/stderr 捕获与应用程序相关的信息,并推送到集中式 Stackdriver 日志系统。

总结

本章介绍了完全托管的应用平台 App Engine 的高层次概况。在这个环境中,许多与开发相关的基础设施任务会自动执行,开发人员无需过多干预。

一般来说,GAE 部署是一个完全托管的活动,构建、托管或执行代码时几乎不需要任何交互。该环境通常由负载均衡器、计算层和服务层组成,这三者协同工作,为开发人员提供集成的应用平台。GAE 提供了一个低投入的开发环境,旨在为开发人员完成大部分繁重的工作。

现在我们对 App Engine 环境有了大致了解,下一章将通过代码示例进一步完善我们的经验和技能。

问题

  1. 任务队列支持哪种类型的服务调度?

  2. Memcache 支持的两个服务级别是什么?

  3. Cloud Datastore 是什么类型的数据库?

  4. 列举 GAE 支持的运行时语言之一。

  5. GAE 支持哪些形式的流量分割算法?

  6. GFE 与 GAE 的关系是什么?

  7. 列举 GAE 支持的三种扩展方式。

  8. 为提高效率,使用了哪种机制将长期运行的工作负载与 HTTP 请求/响应生命周期隔离?

进一步阅读

第二章:使用 App Engine 进行开发

在使用 Google Cloud 时,你自然需要访问云环境。在接下来的章节中,我们将学习如何构建部署在 Google Cloud 上的 App Engine 应用。

现在我们对构成Google App Engine (GAE) 环境的各个组件有了较为深入的了解,在本章中,我们将探讨如何使用 GAE 来开发满足定义要求的应用。我们将从构建一个简单的应用开始,演示在 App Engine 上部署代码的过程。接着我们将学习如何执行应用更新。最后,我们将探索如何使用 Stackdriver Logging 的功能来帮助分析缺陷。所有这些课程都将附有示例和活动,帮助你独立应用这些 GAE 的概念和原则。

GAE 支持多种语言运行时;在本章中,我们将利用 Python 来演示 Google Cloud 的能力。使用不同语言进行开发可能会让人感到畏惧;然而,本章的目的不是教你 Python,而是探讨在不同场景下使用 GAE。

在本章中,我们将涉及以下内容:

  • 在 GAE 上构建应用

  • 理解部署策略

  • 排查 App Engine 部署问题

在 GAE 上构建应用

为了开始,我们将考虑构建第一个 GAE 示例应用所需要的工具和资源。

在本节中,我们将把源代码部署到 GAE 上。作为这一过程的一部分,我们将探讨应用结构以及它与已部署系统的关系。在使用代码时,我们将观察在 Google Cloud Console 中显示和提供的信息。

此外,我们还将探讨如何部署应用(讨论默认版本的含义)、这与代码的关系,以及如何部署多个版本。最后,我们将对部署到系统中的代码执行回滚操作,演示如何完成这项任务,以及这在可用版本方面的含义。但首先,让我们了解一下我们将在本章中使用的 Qwiklabs 环境。

Qwiklabs

Qwiklabs (qwiklabs.com) 提供了一个 Cloud 沙盒,Google Cloud 项目可以在其中运行。使用像 Qwiklabs 这样的环境的目的是给你提供一个接近真实世界工具和服务的体验。为了方便这一过程,Google Cloud 提供了一个临时帐户,并分配了一个时间限制的沙盒环境。一旦创建,Google Cloud 项目将为你提供所有相关的访问权限和服务,这些是你在典型方式下创建的项目所应具备的。

入门指南

我们假设你已经在 Google Cloud 上注册,或者拥有一个有效的 Qwiklabs 账户,能够访问 Google Cloud 沙盒环境。如果没有账户,依然可以跟随操作,但如果能够参与我们讨论中的练习,将会更加有趣。

初次看到 Google 的 Cloud Console 可能会让人感到有些压倒性;然而,界面提供了一个很好的方式来导航众多的服务。在左侧导航菜单中启动 App Engine,我们将带你进行一场体验之旅。

为了开始开发,让我们使用 Google 提供的现有 App Engine 代码,理解一个基础应用程序应该是什么样子。按照以下步骤操作:

  1. 从你的 Google Cloud 账户中,选择 App Engine 菜单项开始创建应用程序。

  2. 初始屏幕将显示为 仪表盘,在这里你可以看到与 App Engine 相关的所有内容的汇总,例如服务、任务队列和防火墙规则。

  3. 现在选择“创建应用程序”按钮:

接下来我们需要决定的是应用程序将在哪个区域运行。让我们看看接下来如何操作。

选择一个区域

从下面的屏幕列表中可以看到,有很多选项可供选择。通常,标准做法是选择一个尽可能靠近数据访问位置的区域。在本示例中,这一标准不适用,因此请执行以下步骤:

  1. 选择 us-east1 作为区域。

  2. 选择“创建应用程序”按钮:

现在我们需要决定使用哪种语言运行时。

选择语言运行时和环境

对于我们的第一个简单示例,我们将使用 Python 运行时。按照以下步骤操作:

  1. 选择“语言”下拉列表。

  2. 从出现的各种语言中,点击 Python。

  3. 从“环境”下拉列表中选择“标准”(这是默认选项,但也可以在开发周期的后期进行更改):

请注意,有多种语言运行时可供选择。如果你不熟悉 Python,别担心:它的布局和特性非常易于理解。在大多数情况下,使用其他语言将遵循我们所概述的相同过程;但是,语言规范会与 Python 示例中描述的有所不同。

完成后,我们可以开始使用 Google Cloud SDKGCloud SDK)。

使用 GCloud SDK

GCloud SDK 提供了一种简单的方式来对 Google Cloud 进行身份验证。将 SDK 安装在本地(即本地环境)提供了显著的好处,因此,如果你希望与其他 Google Cloud 服务集成,它是一个非常好的选择:

  1. 从 Google Cloud Console 左侧选择 App Engine 菜单选项。

  2. 在接下来的屏幕中,我们需要选择项目所需的资源:

  1. 我们不需要 Cloud SDK,因为我们将使用包含预加载 Cloud SDK 的 Cloud Shell。所以通过选择 “I'LL DO THIS LATER” 来跳过这个屏幕。

从前面的部署屏幕中,你还会获得一个命令行提示,使用 gcloud 命令部署代码。强烈建议学习如何使用 gcloud sdk 命令执行不同的任务。

恭喜!此时,你已经成功在 Google Cloud 上创建了你的第一个 GAE 应用程序。正如你所见,这个过程非常简单,旨在确保开发人员尽量减少基础设施管理所需的时间:

请花一点时间记住,对于每个创建的应用程序,都需要类似的过程来创建基础设施。正如你所看到的,这个过程并不复杂;成功完成后,它会输出一条重要的消息,指示整体状态。

构建和部署示例应用程序

在本节中,我们将部署一个标准的 Hello World 应用程序到 GAE。幸运的是,Google 的工程师们为我们提供了帮助,我们可以使用一些示例代码。在接下来的构建和部署阶段,我们将研究这些示例代码。

要开始这个过程,请按照以下步骤操作:

  1. 在你的项目中打开 Cloud Shell 窗口,并从 Google Cloud 仓库克隆 App Engine 的示例代码。

如果你需要在 Google Cloud 上编码一个应用程序,首先查看 Google 的示例仓库,看是否已有现成的示例代码作为模板,这样会更加高效。

  1. 从打开的 Cloud Shell 窗口中,在命令行输入以下内容:
git clone https://github.com/GoogleCloudPlatform/python-docs-samples
  1. 代码位于一个名为 hello_world 的目录中。所以,输入以下命令以进入正确的源代码目录:
cd python-docs-samples/appengine/standard/hello_world
  1. 现在我们有了一些源代码,接下来让我们通过在 Cloud Shell 提示符下执行目录列表命令来查看该目录中的内容:
ls -la

值得注意的是,这里有两个 Python 文件和一个 app.yaml 文件。这样的结构很常见,所以值得概述一下这些文件的作用:

文件名 用途 描述
main.py 应用程序 这个用 Python 编写的文件表示要运行的源代码,并向用户输出一条 Hello, World! 消息。
main_test.py 单元测试 这个文件是主源文件的测试用例,用于确保返回的响应符合预期。
app.yaml 配置文件 这个文件是应用程序的配置文件。数据的内容包括对语言运行时、API 版本和要访问的 URL 的引用。
  1. 要将源代码部署到 GAE,请在 Cloud Shell 命令行中输入以下内容来使用你的代码:
gcloud app deploy ./app.yaml
  1. 当部署命令处理配置信息时,完成后会提供一个 URL,通过该 URL 可以访问正在运行的应用程序:

要查看已部署的应用程序,可以从 Cloud Shell 使用gcloud app browse,或者使用 App Engine 仪表板查看分配的 URL。

一旦 App Engine 完成部署,应用程序就可以从浏览器访问。通常,访问已部署应用程序的 URL 将是[PROJECT_ID].appspot.com的形式。如果你使用了自定义域名,则PROJECT ID部分将被自定义域名替换。

  1. 通过浏览器访问此 URL;这将显示来自我们应用程序的Hello, World!消息,如下图所示:

  1. 现在看看更新后的 GAE 仪表板,观察一旦应用程序部署后出现的变化:

  1. 从仪表板上,我们可以看到应用程序的所有相关遥测数据。我们现在对 GAE 有了出色的工作知识,并了解如何在 Google Cloud 上部署一个简单的应用程序。

干得好!我们已经快速浏览了很多信息,现在让我们慢下来一点,回顾一下本章到目前为止学到的内容。

总结

Hello World!示例中,我们了解了在 GAE 上部署代码的以下方面:

  • 我们查看了如何创建一个简单的 GAE 应用程序;我们使用 Google Cloud 控制台启动构建和部署过程。

  • 我们看到有多个可用区域,并了解了可以访问这些信息的地方。

  • 我们为应用程序选择了 GAE 标准环境和 Python 语言运行时,注意到如果需要,我们也可以选择其他替代方案。

  • 我们克隆了 Google Cloud 存储库,以便访问一些基于 Python 的示例代码。

  • 我们浏览了目录中的系统,看到我们有三个可用的文件,并花时间概述了每个源文件的用途。

  • 最后,我们通过 Cloud Shell 部署了代码,并通过部署过程完成时显示的 URL 通过浏览器界面访问了正在运行的应用程序。

正如你在 App Engine 的第一部分中看到的,部署代码是一个非常简单的过程。在这个环境中,使用 GAE 提供了开发人员所渴望的简洁性。

避免基础设施管理开销的能力可以显著提高效率,并使我们能够更加专注于特定的开发活动。在接下来的部分中,我们将探索 GAE 如何处理各种部署策略。

理解部署策略

GAE 允许部署新版本,并执行更多复杂的操作,例如流量分配。由于我们已经通过命令行部署了一个简单的应用,现在让我们继续使用这个代码库,看看如何通过 GAE 管理控制台实现新版本。

理解控制代码流动的 GAE 控制台背后的机制是非常宝贵的。能够通过发布操作部署新系统为应用环境提供了良好的控制层级。更进一步,我们还可以在版本之间分配流量,以执行复杂的 A/B 测试。

创建新版本

回到 Cloud Shell,仍然在处理我们的源代码,我们将编辑main.py文件,并将消息修改为:Hello, Serverless World!。这些简单的更改将代表我们的新版本应用。

Google Cloud Shell 还包括一个基于 Eclipse Orion 的代码编辑器,用于进行简单的文件编辑。这个编辑器的引入对于需要查看项目中文件的情况非常有帮助。或者,Cloud Shell 也包括其他编辑器(例如 Vim 和 Nano),所以你可以根据个人喜好选择使用。

在上一节中,你可能注意到,在编辑源文件后,我们使用 GCloud SDK 部署了代码。现在我们已经修改了源代码,请按照相同的流程再次操作:

  1. 在 Cloud Shell 中,运行以下命令:
gcloud app deploy ./app.yaml
  1. 新的应用版本已经部署,无需额外的工作。

  2. 回到 Google Cloud Console 和版本菜单。

我们可以看到,新的应用版本已经部署,并且可以从列表中获取:

请注意,在前面的输出中,流量分配已经自动切换到新版本。这种行为在大多数情况下是可以接受的;然而,如果你不希望流量自动升级到最新版本来部署新代码,请使用--no-promote标志选项。

请注意,使用的版本字符串代表应用的时间戳。此外,应用的两个版本仍然可以轻松地从控制台访问——默认版本会跟踪流量使用的修订版。如果你希望切换到特定的版本字符串,可以在部署过程中使用--version标志选项指定。

在这一点上,你可能想将这个简单的部署过程与典型的基础设施项目进行对比,在这些项目中,易用性并不像这里这么明显。GAE 部署的简便性令人惊叹,其精心设计使得只暴露出开发所需的部分。

现在我们已经完成部署,可以查看一些 App Engine 默认提供的优秀功能,从流量管理开始。

流量分配

现在我们有两个版本在运行,如果我们能将流量在它们之间分配岂不是很棒吗?考虑到这种选项在测试环境(例如 A/B 测试)或生产环境(例如分阶段发布)中的实用性。再次强调,这种简单性不应被误解;这是一种非常强大的选项,毫无疑问,它将被一次又一次地使用。

要拆分流量,请按照以下步骤操作:

  1. 进入 GAE 云控制台,在“版本”菜单下,查找特定版本的“拆分流量”选项并选择它。

  2. 现在,将会提供一系列选项来拆分我们的流量,正如前一章所述(即 IP 地址、Cookie 或随机):

我们可以看到我们的新应用版本已默认接受 100%的流量分配。为了演示,我们希望它只接收 25%的流量。为此,请执行以下步骤:

  1. 点击“添加版本”按钮,选择将消耗 75%流量的备用应用版本。你会看到我们仍然有原始版本的已部署应用可用,所以让我们将流量导向它。

  2. 现在从版本旁边的框中选择剩余的百分比:

  1. 一旦选择了流量拆分比例,点击保存按钮,GAE 会显示它正在动态更新项目中的流量拆分配置。

  2. 返回到“版本”屏幕,查看项目实际上在已选择的应用程序之间按之前选定的比例划分流量:

正如我们预期的那样,前面的操作,例如流量拆分,也可以通过 GCloud SDK 来实现。要通过 SDK 实现相同的结果,请使用gcloud app set-traffic选项,以便从脚本或命令行界面获得灵活的方式来实现预期的结果。

现在我们知道如何在应用版本之间启动流量拆分,接下来我们来看看如何迁移流量。

迁移流量

最后,在这一部分,我们希望将所有流量迁移回我们应用的原始已部署版本。到现在为止,我猜你已经有了如何实现这一目标的想法。

在“版本”屏幕的顶部,有一个“迁移流量”选项。我们将使用它将所有现有流量流量迁移回原始版本,如下图所示:

注意,原始应用程序当前接受 75%的流量,新版本只接受 25%的流量。

迁移将确保流量被定向到选定的默认版本。在这种类型的迁移中,目标实例只能是单一版本:

使用 GAE 进行应用程序版本之间的流量迁移非常简单。为了巩固我们的理解,接下来我们将总结所学内容。

总结

在本节中,我们学习了如何在 GAE 上控制部署。我们使用了几个内置工具,并学习了在处理应用程序时如何使用它们:

  • 我们看到了 GAE 如何对应用程序进行版本控制。

  • 我们观察了 GAE 上实时流量分割,只需点击按钮即可访问。

  • 我们使用迁移流量选项来执行简化的应用程序回滚。

在本节中,我们探讨了流量分割如何成为一个宝贵的开发者工具,用于测试应用程序功能。在这方面,使用 GAE 显得格外简单,因为它提供了一种复杂的方式来执行 A/B 测试,无需任何额外的设置或要求。

此外,我们执行了版本回滚,允许我们快速在不同的流量服务版本之间切换,并返回到指定的默认版本。在这种情况下,我们无需考虑如何管理底层基础设施;GAE 为我们完成了繁重的工作。下一节中,我们将探讨如何让构建和部署过程更加一致,减少出错的可能性。

故障排除 App Engine 部署

在本节中,我们将通过使用 Stackdriver Logging 更详细地检查应用程序故障排除。Stackdriver 将应用程序信息汇总到一个单一视图中,方便开发者分析数据。

调试应用程序可能非常耗时,需要一定的技能来从噪声中找出有用的信号。GAE 的日志在 Stackdriver 中完全可用,学习如何使用该工具可以为识别应用程序中的缺陷提供宝贵的帮助。

构建与部署

打开 Google Cloud 控制台中的 Stackdriver Logging,展示与 Google Cloud 上汇总的日志相关的主要信息页面。Stackdriver Logging 有两种模式:

  • 基本模式

  • 高级筛选器

基本模式是默认模式,在我们的示例中,我们将使用它来展示部署应用程序的一些关键信息。高级筛选器使用户能够创建自定义筛选器,用于分析日志中的信息。

基本模式

在 Stackdriver Logging 中,我们首先需要筛选信息,并将其限制为 GAE 应用程序相关的数据,这样它就专门与我们关注的系统部分相关:

在前面的截图中,过滤器的修改包括 GAE 应用和所有日志。从显示的信息可以看出,这是对在 GAE 上发生的活动的非常详细的概述。例如,我们可以看到几个与 CreateApplicationCreateVersionUpdateService 活动相关的条目。每个活动都有一个相关的有效载荷,其中包含更多的详细信息,因此在需要时可以进一步获取信息。

此外,页面接收到的(HTTP)响应代码也会显示在日志中,这意味着可以轻松创建基于日志信息的应用程序可用性监控指标。例如,过去一小时内收到过多少次 404 错误 是一个非常有用的指标。聚合指标是一种智能的应用程序分析方法,因此只有事件发生的频率可以作为调查的基础。

高级过滤器

在某些情况下,调查 GAE 上的操作时需要一个更定制的过滤器。在这种情况下,使用高级过滤器。通过下拉过滤器访问高级日志过滤器,它提供了一个更高效的界面,您可以在其中选择从 Stackdriver 中获取的日志信息。

例如,在此模式下,选择 CreateApplication 项会填充高级过滤器,并根据用户提交的标准显示一个更有针对性的条目列表。

Stackdriver 是一个非常有价值的服务,用于主动管理应用程序:

  • Stackdriver Logging:访问日志信息以进行问题分析

  • Stackdriver Monitoring:访问监控信息以解决一般问题

  • Stackdriver Trace:访问延迟问题的追踪信息

  • Stackdriver Debug:访问应用程序详细信息以解决缺陷问题

第一层防御涉及集成日志记录,以提供已部署应用程序的详细和准确视图。此类示例包括 Stackdriver 监控、追踪和调试,它们利用关于应用程序的信息来帮助进行持续的维护活动和已部署构件的改进。

总结

在本节中,我们重点介绍了将 Stackdriver Logging 与 GAE 配合使用的第一步。随着我们深入更高级的主题,我们将进一步探讨 Stackdriver 的这一功能:

  • 我们创建了一个 基本 模式过滤器,以查看特定的 GAE 信息。

  • 我们切换到 高级 过滤器,以更细粒度地查看 GAE 应用程序日志信息。

  • 我们概述了一些 Stackdriver 功能,并在高层次上讨论了如何使用这些关键特性。

学会如何在 Google Cloud 上使用 Stackdriver 是提高开发者生产力的基础。随着应用程序复杂性的增加,能够在一个易于访问的仪表盘中聚合来自多个服务的信息,既令人信服又非常有用。

小结

在本章中,我们涵盖了许多内容,以便处理使用 GAE 的一些典型示例。到现在为止,您应该已经很好地理解了架构及其相关组件如何交互,并能够避免在特定情况下重复造轮子。

我们从讨论如何在 GAE 环境中部署应用程序开始。还涉及了如何使用版本控制和源配置来支持多个环境。最后,我们探讨了不同的部署策略,并首次了解了 Stackdriver Logging。

我们已经看到,GAE 非常适合托管需要 HTTP(s)/API 访问的解决方案的应用程序。无论是 GAE Standard 还是 Flex,都为 Google Cloud 中的无服务器应用程序提供了有力的支持。从开发者的角度来看,几乎不需要进行基础设施管理。

在下一章,我们将扩展对 Google Cloud 无服务器产品的理解,并开始关注基于事件的选项。在选择替代方案时,考虑某种用例相较于其他用例的优势总是一个好主意。现在我们已经探讨了应用程序,我们将关注更细化的需求,并通过函数引入事件处理。

问题

  1. GAE 有三个版本。对还是错?

  2. GAE 提供哪些流量拆分选项?

  3. Stackdriver Logging 提供了哪些过滤日志选项?

  4. 在创建 App Engine 应用程序时,您需要选择一个区域。对还是错?

  5. 流量迁移提供了一种简单的应用程序回滚方法。对还是错?

  6. 执行 GAE 命令行部署的命令是什么?

  7. App Engine 部署的 URL 中包含哪些常见属性?

进一步阅读

第二部分:Google Cloud Functions

在本节中,您将学习关于云函数及其特性,这些特性使其成为构建轻量级函数的一个引人注目的产品。最初的讨论概述了在 Google Cloud 上使用函数的核心属性。接下来的章节提供了产品能力的概览,并介绍了与 Stackdriver 和 Cloud Build 等服务的集成。最后,我们在前面章节的示例基础上,构建一个小型应用程序,以扩展在四个关于 Google Cloud Functions 章节中涵盖的概念。

本节包括以下章节:

  • 第三章,介绍轻量级函数

  • 第四章,开发云函数

  • 第五章,探索函数即服务

  • 第六章,云函数实验室

第三章:介绍轻量级函数

在简要介绍了Google App EngineGAE)之后,我们现在可以将注意力转向完全托管的事件处理。本章中,我们将开始使用 Google Compute Engine,并探索如何在我们的环境中集成无服务器操作管理工具。更具体地说,事件处理的内容,包括 Cloud Functions,从这里开始。

本章将首先简要概述 Cloud Scheduler 和 Cloud Tasks,它们是为 Google Cloud 提供支持的服务,接着介绍 Cloud Functions,然后我们将开发一个小应用来利用这些服务。

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

  • 操作管理工具

  • Cloud Tasks 和 Cloud Scheduler

  • Cloud Functions 简介

  • 开发一个 Cloud Functions 应用

技术要求

为了完成本章中的练习,您需要一个 Google Cloud 项目或 Qwiklabs 账户。

您可以在本书的 GitHub 仓库中找到本章的代码文件,位置在 ch03 子目录下,链接为 github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch03

在您阅读本书中的代码片段时,您会注意到在某些情况下,代码/输出中的几行被删除,并用省略号(...)替代。省略号的使用仅用于显示相关的代码/输出。完整的代码可在前面提到的 GitHub 链接中找到。

操作管理工具

能够响应外部生成的事件提供了一种灵活的机制,用于构建易于扩展和增强的系统。考虑一下,您如何设计一个解决方案,根据提交的图形表示生成缩略图。首先,我们需要一些磁盘存储来保存图像,然后我们需要处理图像以生成缩略图,最后我们需要更多的磁盘存储来保存输出。

该活动的每个阶段都需要状态变化和响应。状态变化是与服务调用相关联的通知,用于指示资源的状态;例如,图像已存储,图片已更改,或者处理已完成。

在本章后面介绍的 Cloud Functions 示例中,一个事件触发了在Google Cloud StorageGCS)中新项目的存储。该函数通过事件触发器被提醒,并能在此事件的响应中执行一系列指令。在这些指令的开始阶段,转换后的图像(现在是缩略图)会提供给最终用户。

在这一点上,经过前面基于 GAE 的章节,你已经对 Cloud Tasks 有了一定的了解。选择服务之前,始终考虑目标是很重要的。采取这一步骤将帮助你判断该技术是否适合你的需求。

因此,本节不会进行详细探讨,因为这些是使 Google Cloud 上无服务器工作负载成为可能的辅助服务。相反,我们将概述关键点,以提供背景和可能的用例。通过这种方式,处理本节内容应该有助于做出关于是否纳入这些服务的智能决策。在许多情况下,会存在模糊地带,以及没有明确选择的情况。幸运的是,这些情况会是例外,因此我们可以在很大程度上概括如何处理这种类型的情况。我们的讨论从 Cloud Tasks 开始。

Cloud Tasks

我们已经在第一章《介绍 App Engine》和第二章《使用 App Engine 进行开发》中回顾了使用 Cloud Tasks 与 GAE 的好处。许多这些优势在与 HTTP 目标协作时仍然存在,例如通过将长期请求卸载到工作进程来减少应用程序延迟,或者减少面向用户任务的流量峰值。

分布式任务在 Google Cloud 上的服务专门用于管理任务的执行、调度和交付。其理念是,能够卸载、隔离并管理 HTTP 请求以最小化延迟是更可取的。这个异步工作流在处理需要响应式接口的用户面向应用时尤其有用。为任务所做的消息负载请求必须包括 POST 或 PUT 作为要处理的 HTTP 方法。

Cloud Tasks 提供了除了异步卸载任务的优势之外的多个好处。此外,它还允许开发人员配置与数据传输相关的各种属性。这可能包括复杂的策略,如应用重试或在任何数据契约上设置速率限制。

简而言之,以下元素是 Cloud Tasks 的重要特性,并为使用该服务提供了有力的理由:

  • 异步任务卸载

  • 可配置的重试策略

  • 可配置的速率限制

  • 延迟调度

  • 解耦服务

  • 提高对失败的抗压能力

  • 可扩展且完全托管

  • HTTP 目标

  • 保证交付

  • 支持多种流行语言

通过构建一个数据元素队列,Cloud Tasks 为工人处理信息提供了一种高效的方式。每个队列都有速率限制,决定了每个队列将执行多少任务。由于应用了保证交付机制,如果发布的消息的响应不符合应用要求,则会重试。一般来说,响应代码遵循 HTTP 规范,其中 2xx 范围的状态代码表示成功。

分布式任务队列使应用程序在执行异步操作时更加响应。任务队列能够通过利用关键产品功能,如调度、去重、重试策略和版本重定向,来组织和控制请求。

解耦服务(例如 Cloud Tasks 和 Cloud Pub/Sub)可以提供更好的结构和应用扩展性,特别是在使用微服务时。在这种情况下,任务处理程序将驻留在每个专用服务中,以便微服务能够独立扩展。因此,资源消耗的管理控制确保了更好的负载动态,从而实现更流畅的服务访问,例如速率限制队列。

此外,在优雅地处理发布和事件时,请求可以暂停、重试并重定向到新版本。如果你曾使用过 Cloud Pub/Sub,那么这种解耦方式可能非常熟悉。这两种服务之间的主要区别在于调用方式。Cloud Tasks 使发布者能够控制执行,即端点。而 Cloud Pub/Sub 使订阅者能够控制消息传递。

要了解这两项服务之间的差异,请阅读关于选择 Cloud Tasks 和 Pub/Sub 之间的文档,参考链接:cloud.google.com/tasks/docs/comp-pub-sub

Cloud Scheduler

将 Cloud Scheduler 看作是云中的 cron 作业。如果你不熟悉 cron 作业,它们代表了一种基于时间表在机器上安排活动的简单方式。作为一个统一的管理界面,Cloud Scheduler 使用户能够从一个地方管理他们所有的自动化需求。创建 Cloud Scheduler 作业只需少量元素即可运行。

如果你处于需要自动化某些云基础设施操作的场景中,那么这是实现该目标的完美方式。作为一项完全托管的服务,Cloud Scheduler 提供了一个简单的界面,你可以通过它来设置和配置你的自动化需求,具体如下:

  1. 确定任务运行和触发的频率。

  2. 确定如何调用 Cloud Scheduler(即 Pub/Sub、App Engine HTTP 或 HTTP)。

  3. 部署服务后,任务将在计划时间自动触发。

  4. 通过 Cloud Scheduler 控制台页面查看每次调用的结果,或者通过 Stackdriver Logging 访问它。

Cloud Scheduler 提供了一种简单有效的方式来定期执行任务。如果你曾经需要运行备份或下载更新等任务,你就会知道这个应用程序有多么有用和强大。作为一款企业级产品,Cloud Scheduler 提供的本质上是基于云的 crontab。这个产品的魅力在于,它使用户能够在现有的 Google Cloud 目标服务中触发任务。拥有这些集成目标提供了一个高可靠性的机制,确保定期执行的任务能够支持现有的使用模式。

除此之外,Cloud Scheduler 还提供了一些关键元素,下面的列表中会涵盖:

  • 保证可靠的交付,即至少保证一个作业目标的交付(这确实带来了一些复杂性,例如数据排序可能成为一个问题,类似于使用诸如 Cloud Pub/Sub 之类的消息解决方案)。

  • 支持广泛的服务目标,如 App Engine、Cloud Pub/Sub 和 HTTP 端点。

  • 重试和回退机制为作业需求确定合适的重试策略。

  • 与 Stackdriver Logging 的集成捕获与任务执行和性能相关的信息。

  • 支持 Unix cron 格式,因此你现有的知识是可以迁移的。

所以现在我们知道了这些,我们可以快速创建一个 Cloud Scheduler 演示,展示如何使用它。在以下示例中,Cloud Scheduler 将使用 Cloud Pub/Sub 来说明它如何与定义的端点进行交互。

第一个任务是创建一个 Pub/Sub 主题。Pub/Sub 主题将根据分配的订阅者整理准备分发的消息,如下所示:

  1. 在 Google Cloud Console 菜单中,选择 Pub/Sub 选项。

  2. 在 Pub/Sub 屏幕中,选择创建主题菜单选项。

  3. 在创建主题屏幕上,输入主题 ID 为cron-topic

  4. 保留加密设置为以下默认选项:Google 管理的密钥;然后选择 CREATE TOPIC 按钮:

一旦我们定义了主题,接下来需要为其建立订阅。对于任何可用的消息,我们可以使用拉取或推送机制来访问与主题相关的数据负载:

  1. 现在我们需要为主题创建一个订阅,因此请从左侧菜单中选择订阅选项。

  2. 在屏幕顶部,选择 CREATE SUBSCRIPTION 选项。

  3. 输入订阅 ID 为cron-sub

  4. 输入主题名称为projects/<project-id>/topics/cron-topic,将<project-id>替换为你系统中的 Google Cloud 项目 ID。

  5. 然后选择屏幕底部的 CREATE 按钮。

最后,我们需要定义将使用之前创建的 Pub/Sub 作为端点的 Cloud Scheduler。每次 Cloud Scheduler 任务调用时,它将向 Pub/Sub 发送负载:

  1. 选择 Cloud Scheduler 选项(位于工具下),然后选择 CREATE JOB 选项。

  2. 结果应类似于以下屏幕:

  1. 填写以下表格中的字段:
字段 内容
名称 cron-task
描述 Google Cloud 上 Cloud Scheduler 的演示
频率 * * * * *
时区 英国(格林威治标准时间)
目标 Pub/Sub
主题 cron-topic
负载 Yeah Cloud Scheduler Rocks
  1. 完成填写上述字段后,选择“创建”按钮,将你的 Cloud Scheduler 任务添加到活动作业列表中。

示例代码将每分钟运行一次,但为了简洁起见,请按下“立即运行”按钮。从 Cloud Scheduler Jobs 页面,你将看到结果类别被记录为成功,并显示任务最后一次运行的时间。如果你现在查看 Cloud Pub/Sub 队列并点击 cron-topic,在“查看消息”选项下,你可以获取在 Cloud Scheduler 中添加的负载信息。或者,你也可以使用 Cloud Shell,通过以下 gcloud 命令访问 Pub/Sub 订阅:

gcloud pubsub subscriptions pull cron-sub --limit 10

恭喜!如你所见,Cloud Scheduler 实现了与 Pub/Sub、App Engine 和 HTTP 的简单集成。

现在我们已经讨论了启动任务和调度活动的方法,接下来可以开始考虑使用功能处理信息的更广泛的系统需求。

介绍 Cloud Functions

Cloud Functions 平台提供了一种有效的方式,在云中运行单一功能的代码。记住,独立服务不需要管理服务器或其相关的运行时环境。事件通知的发生会触发单一功能。轻量级的 Cloud Functions 平台为基于事件驱动的无服务器计算提供了基础。

构建独立调用的单一功能提供了一个高度可用的架构,用于构建你的服务。将这些功能组合起来是一种扩展云服务的优秀方式。这些单一功能提供了轻量级组件,从而为维护应用程序提供了一种更自然的方式。没有服务器配置和补丁/更新周期的部署和维护简化,可以成为设计解决方案的有效方式。

连接云服务并与不同接口(如 Webhooks、API 以及 物联网IoT)设备)进行交互的能力是非常理想的。Google 使得 Cloud Functions 的接口非常易于使用;你可以构建单一功能,唯一的限制是你的想象力。

在书中的后面章节,我们将讨论如何使用 Functions Framework 开发代码,创建轻量级的函数。Functions Framework 是一个开源项目,它可以实现跨多个环境(例如 Google Cloud Functions、Cloud Run 和 Knative)的无缝兼容。目前请注意,在构建函数时,这种方法提供了一种兼容性,使你可以在技术栈之间无缝切换。

无服务器计算平台广泛的应用场景使其成为一个具有吸引力的选择。通常,它作为服务和 API 之间的粘合剂被部署,Cloud Functions 为任何希望在以下领域使用 Google Cloud 的人提供了一个可扩展的层:

  • 实时文件处理

  • 事件驱动的提取、转换和加载管道

  • 无服务器 IoT 后台

  • 通过 API 进行第三方集成

典型的使用场景可能包括以下内容:

  • 物联网(IoT):与设备的无服务器交互,通过事件流处理信息。Cloud Functions 提供一个简化的接口,可以作为一个强大的工具,用于收集和分发批量或实时信息,利用像 Cloud Pub/Sub 这样的服务。

  • API:HTTP 触发器可以设计聚合逻辑来响应应用程序调用。这种轻量级的 API 非常适合抽象事件链中更复杂的处理,如存储或队列需求。构建此类 API 可以通过事件驱动接口和 HTTP 来实现。

为了说明如何创建一个简单的 Cloud Functions 应用程序,在下一节中,我们将详细讲解这个过程的主要步骤。

开发基于 Cloud Functions 的应用程序

为了帮助我们理解之前讨论的服务,我们将开发一个小型应用程序来利用这些服务。我们的 Cloud Function 将展示一个简单的网页,其中一些基于 Web 的信息显示输出。为了展示之前讨论的每个函数的强大功能,下面表格中的每个需求里程碑将逐步改变并改善整体设计。

我们的应用程序将使用 Google Cloud Functions 进行开发,并在屏幕上显示一些信息。顺便提一下,如果你需要托管类似静态网站的内容,可以通过使用 GCS 存储桶快速完成。存储桶非常灵活。有关如何实现它们的更多信息,请查看文档:cloud.google.com/storage/docs/static-website

我们将在接下来的几个章节中开发一个 RSS 阅读器应用程序。我们的高级需求是将来自特定网站的信息以 HTML 网页的形式呈现。我们的高级需求如下:

参考 需求 描述
1 开发一个 Cloud Function。 部署一个单一函数应用。
2 从 RSS 网站读取内容。 以 BBC 的 RSS 订阅源为例。
3 将数据渲染为 HTML。 将需要消费的信息以 HTML 格式呈现。
4 刷新数据。 使数据能够按照自动化的时间表刷新。

现在我们已经准备好了所有要求,可以继续构建应用程序的第一次迭代。

应用程序版本 1 – 引入 Cloud Functions

在这个示例中,我们将一步步演示如何使用 Cloud Functions 作为应用程序的基础。这个过程包括以下步骤:

  1. 创建函数:从一个简单的 Cloud Function 开始。

  2. 添加函数框架:了解在将代码迁移到 Cloud Shell 时如何添加其他库。

  3. 部署生成的代码:学习如何通过命令行部署代码。

从 Cloud Functions 开始变得更加容易,因为它提供了多种语言的模板代码。包含这些模板代码对于开始时非常有帮助,并使开发者能够快速尝试该服务,而不必花时间设置环境。

在处理高层次需求时,从外部网站检索 RSS 源似乎是一个关键活动。这个任务看起来也像是一个已有的模式,可以用来实现所需的结果。

作为开发的第一步,快速浏览一下互联网寻找灵感总是值得的。对于这个任务,有相当多现有的包能够访问 RSS 源。构思一个初步设计有助于我们思考想要设计的内容:

在上面的示意图中,我们可以看到 Cloud Functions 被用来访问后端的 RSS 数据源。同时,用户可以访问 HTTP 端点来检索从后端获取的 RSS 信息。

以下示例将使用 Google Cloud 中的 Cloud Functions 选项。为了加速开发周期,我们将利用现有组件来构建应用程序,并尽量减少需要编写的代码量。为了处理 RSS 源,我们将使用一个现有的包来处理所有的处理工作。该包还需要能够访问 RSS 源中的元素。在本示例中,我将使用 Node.js;不过,你也可以使用其他语言,如 Python 或 Go。

在这一点上,我们将使用 Cloud Console 来编写和部署代码到 Cloud Functions:

我们从使用 Cloud Functions 提供的标准模板代码开始,来创建一个应用程序。使用这个模板可以创建 Cloud Functions 并附加不同的属性:

选择 Google Cloud Console 中的 Cloud Functions 选项并打开一个新的 Cloud Function 后,选择 Node 作为运行时语言,这样应用程序代码就会预填充示例的模板代码,如下图所示:

要通过 Cloud Functions 将一些任意文本打印到控制台日志中,我们可以执行以下操作:

  1. 编辑 Cloud Functions 模板中呈现的 index.js 文件。我们将通过修改 Google 提供的蓝图代码并添加一些代码来执行以下任务,来启动我们的应用程序:
/**
* Responds to any HTTP request.
*
* @param {!express:Request} req HTTP request context.
* @param {!express:Response} res HTTP response context.
*/
exports.helloRSS = (req, res) => {
  console.log('helloRSS - display some info');
  res.status(200).send('helloRSS');
};

在前面的代码中,我们引入了 console.log 语句,它将信息写入日志基础设施。对于 Google Cloud 来说,这将是 Stackdriver Logging。

  1. 要部署前面的代码,我们需要点击屏幕底部显示的“创建”按钮。

部署此代码会返回一个 helloRSS 消息作为 HTTP 响应。此外,这些信息还可以在 Stackdriver 中查看,因为它作为所有捕获日志的集中存储库。

  1. 要从 Stackdriver 获取信息,请使用 resource.type = cloud_function 过滤器来访问与程序执行相关的特定数据。

第二章,使用 App Engine 开发,有一节内容概述了如何在 Stackdriver 中进行过滤。如果你需要提醒如何执行这个任务,可以查看这一部分。

现在我们有了一个非常基础的功能。为了在接下来的部分进行扩展,我们将引入一些外部包来渲染视图。

应用程序版本 2 – 迁移到 Cloud Shell

随着我们的代码变得更加复杂,我们应该抓住机会将开发迁移到更便捷的地方。这里的“便捷”当然是指命令行。现在我们已经了解了如何通过控制台使用 Cloud Functions,接下来的步骤是使用 Google Cloud 上的 Cloud Shell:

当我们使用 Cloud Shell 时,它会提供许多有用的提示。例如,在前面的截图中,Cloud Shell 表示它不知道应该关联哪个 Google Cloud 项目。为了解决这个问题,可以使用 gcloud config set [PROJECT_ID] 命令。

如果你不知道正确的 PROJECT_ID,可以在 Google Cloud Console 的主屏幕下的项目信息部分找到此信息。

如果你在本书中的任何应用程序代码上遇到困难,还可以使用每一章开始时提供的代码库版本链接。现在我们已经打开了 Cloud Shell,按照以下步骤从头开始重建应用程序:

  1. 首先,为你的 Node 代码创建一个新的目录来存放代码。我们称之为 ch03

在 Cloud Console 中,两个代码文件,index.jspackage.json,作为我们 Cloud Function 的一部分进行部署。可以在 Cloud Functions 控制台查看每个文件的内容。使用你喜欢的编辑器,我们将重新创建这个应用程序。

在后续章节中,我们将更详细地讨论本地开发的其他选项,但现在我们将专注于如何将代码迁移到 Cloud Shell。

  1. 在新的目录中,从云函数控制台窗口复制现有的index.js函数代码,并将其粘贴到位于 Cloud Shell 中的ch03子目录内的新index.js文件中。

  2. package.json文件做同样的操作,现在你应该在ch03开发目录中拥有两个源文件(index.jspackage.json)。

  3. 此外,我们还需要安装任何所需的包。让我们添加functions-framework包:

npm install @google-cloud/functions-framework

如果你偷偷查看目录,会突然发现比我们创建的两个源文件还多了文件。别担心,暂时不用关心。需要提到的关键点是,functions-framework被包含在package.json配置文件中。我们将在下一章关于云函数的内容中更详细地讲解这一点。

现在我们的代码已经在 Cloud Shell 中很好地打包好了,我们可以做一些高大上的事情,例如将其保存到版本控制的仓库中。但我们将继续测试文件是否按预期工作,这意味着我们需要部署代码。

  1. 让我们继续停留在 Cloud Shell 中,使用以下命令部署我们的代码:
gcloud functions deploy tempest --entry-point helloRSS --runtime nodejs8 --trigger-http

系统会询问是否允许未经身份验证的调用;请选择“是”。太棒了,恭喜你!你已经通过命令行部署了一个名为tempest的云函数!目前它的功能不多,但仍然值得庆祝。接下来,我们将扩展应用程序,让它做一些更有用的事情。

应用版本 3 – 添加视图

在我们的应用程序中,我们希望扩展代码以读取 RSS 源。为了在现有代码的基础上进行构建,我们应该加入一些改进,使其更接近我们的应用需求:

  • 显示一些静态内容。

  • 利用 HTTP 响应以 HTML 页面形式展示。

为了实现这一点,我们将执行以下步骤:

  1. 引入一个 NPM 对等包来执行必要的 RSS 交互。对等包是一个在package.json文件中声明的 Node 依赖项。快速搜索 NPM 包后,我们发现rss-parser正是满足我们需求的理想选择。

使用包的一个好处是,它会包含所有关于配置的相关信息。当安装完成时,Node 会自动更新package.json文件。通过添加 RSS 组件,我们能够以最少的代码快速满足读取 RSS 源的需求。

一项好的实践是检查可用的源代码/库。我们永远不应盲目相信包,因此在将第三方代码纳入项目时,必须保持警觉。始终留意显示的与安全相关的漏洞和警告。

查看 NPM 网站上的rss-parser页面(www.npmjs.com/package/rss-parser),在版本部分,我们可以看到当前版本(写作时)是 3.7.2。

  1. 从命令行,我们可以通过执行以下命令来安装这个包:
npm install rss-parser
  1. 现在,通过查看package.json文件,我们可以看到它已被更新,以包括相关的包和版本。
{
  "name": "sample-http",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/functions-framework": "¹.3.2",
    "rss-parser": "³.7.3"
  }
}
  1. 现在package.json文件已经更新以包含rss-parser,在 JavaScript 文件index.js中,我们将声明一个新的异步函数来完成所有繁重的工作,并获取 RSS 代码以输出结果。

我们正在使用异步函数,因为网络调用可能需要一段时间才能处理;我们不希望在请求/响应生命周期的 UI 处理上造成不必要的延迟。

  1. 在这个新函数中,我们首先要引入rss-parser包(www.npmjs.com/package/rss-parser),通过声明一个新对象来消费 RSS 数据源。我们还添加了一个新的后台异步函数,用于处理相关的 RSS 源解析。

我们不再将数据返回给原始函数helloRSS,而是将所有与网络相关的活动处理交给asyncBBCFeed函数。

  1. index.js添加以下代码,如下所示:
let Parser = require('rss-parser');
let parser = new Parser();
// New background function - Async
async function asyncBBCFeed(req, res) {
    let feed = await parser.parseURL('http://feeds.bbci.co.uk/news/rss.xml');
    console.log (feed.title);
    var testString = '';
    // Title
    testString = '<h1>RSS Lab</h1></p>'

    // Loop through the content
    feed.items.forEach(item => {
      console.log(item.title + ':' + item.link);
     // Create a link per title
     testString = testString + '<a href="' + item.link + '">'+item.title + '</a>' + '</br>';
    });
    // Display the feed returned
    res.status(200).send(testString);
}
  1. 现在更新helloRSS函数,使其如下所示:
/**
* Responds to any HTTP request.*
* @param {!express:Request} req HTTP request context.
* @param {!express:Response} res HTTP response context.
*/
exports.helloRSS = (req, res) => {
 var testMessage = '';
 console.log('helloRSS - display some info');
 asyncBBCFeed(req, res);
};
  1. 返回 Cloud Shell,部署更新后的代码:
gcloud functions deploy tempest --entry-point helloRSS --runtime nodejs8 --trigger-http

部署代码后,查看 Cloud Console 中的 Cloud Functions 界面。在这里,我们可以看到应用程序的版本已增至 2。继续触发该函数,查看我们所添加的异步调用的结果。太棒了!一些漂亮的头条已经从 RSS 源中提取出来,如下图所示:

与外部数据源进行交互将是日常使用的一个场景,因此理解如何操作这一点非常重要,同时也要考虑相关设计的关键方面。在应用程序的第二个版本中,我们希望将 HTML 输出到我们的网站。同样,这仍然是一个日常使用场景,且我们没有开发额外的代码,而是使用现有的 Node 组件来为我们处理这一任务。引入异步任务将减少网络交互带来的延迟。

从代码来看,HTML 与系统代码紧密耦合,因此这需要解决。在开始解决这个特定问题之前,是时候将我们的代码从 Cloud Console 移到命令行了。在下一部分,我们将专注于仅使用 Cloud Shell。

应用版本 4 – 解耦 HTML 输出

正如我们现在从使用 Node 中了解到的,它意味着我们可以访问大量的包来做一些酷的事情。对于我们的应用程序,我们可以使用一个名为pug的包,以更可管理的方式渲染 HTML。将给定的文本从 RSS 数据中解耦,给了我们一个机会,以一种不那么复杂的方式展示信息,而无需重新编写 Cloud Function 的主要部分:

  1. 在 Cloud Shell 中,按照之前的示例,我们需要向package.json文件添加一个新的对等依赖:
npm install pug
  1. Pug 将使先前嵌入在 JavaScript 中的 HTML 被分离到一个单独的文件中:
{
    "name": "sample-http",
    "version": "0.0.1",
    "dependencies": {
        "@google-cloud/functions-framework": "¹.1.1",
        "rss-parser": "³.7.2",
        "pug": "².0.4"
    }
}
  1. index.js 文件中,我们需要在应用程序中添加对 pug 对象的引用:
let pug = require('pug');
  1. 接下来,我们在渲染 HTML 输出时,使用 index.pug 文件启动程序化视图(即页面渲染后的样式)。在 asyncBBCFeed 函数中添加以下内容:
const pugInputFile = pug.compileFile('index.pug');
  1. 我们还需要更新响应对象,以指示:
    • 成功处理

    • 要渲染的视图,即 index.pug

    • 值的插值,在我们的例子中,是从异步调用返回的 feed.items

res.status(200).send(pugInputFile({
    items: feed.items
}));
  1. 最后,我们来去掉一些不再需要处理 feed 项目的样板代码。

在更新前面步骤中概述的更改后,我们的 index.js 应如下所示:

let Parser = require('rss-parser');
let parser = new Parser();
let pug = require(‘pug');

// New background function - Async
async function asyncBBCFeed(req, res) {
    let feed = await parser.parseURL('http://feeds.bbci.co.uk/news/rss.xml');
    // Use an external pug file
    const pugInputFile = pug.compileFile('index.pug');
    // console.log(feed.items)
    // Display the feed returned
    res.status(200).send(pugInputFile({
        items: feed.items
    }));
}
/**
* Responds to any HTTP request.
*
* @param {!express:Request} req HTTP request context.
* @param {!express:Response} res HTTP response context.
*/
exports.helloRSS = (req, res) => {
    asyncBBCFeed(req, res);
};
  1. 为了渲染视图,我们现在需要在 view 目录中创建一个名为 index.pug 的新文件,并添加以下内容:
html
  head
    title Template - This is a Pug generated output
  body
    header
      p RSS Lab
    section
    each item in items
      a(href='' + item.link) #{item.title} <br>
  1. 返回 Cloud Shell,部署更新后的代码:
gcloud functions deploy tempest --entry-point helloRSS --runtime nodejs8 --trigger-http

测试已部署的代码应该会产生与前面 应用版本 3 - 添加视图 部分中所示的相同输出。

但是,请注意,我们的应用复杂性显著降低了。而且,我们甚至不需要写太多代码。更新应用程序以应用前面的代码更新后,借此机会尝试另一种部署方式:

  1. 从 Cloud Shell 压缩开发目录的内容到一个 ZIP 文件中。

  2. 让我们在你的项目中创建一个 Cloud Storage 存储桶(存储桶名称需要全球唯一,因此选择一个适合你的名称,并将 [BUCKET_ID] 替换为你的唯一标识符)。

  3. 将文件上传到你的存储桶。

  4. 从 Cloud Function 界面,选择 Create 函数,并从 Cloud Storage 中选择 ZIP 文件。

  5. 从存储桶位置选择 ZIP 文件,并且不要忘记更改 执行的函数 设置——我们使用名为 helloRSS 的函数。

  6. 最后,选择“创建”选项,并为从存储桶中提取的代码选择“创建新函数”选项。

如果你像我一样,手动部署代码的乐趣不会持久。在这种情况下,使用 Cloud Build 会引起你的兴趣。

与事件处理一起工作提供了一种简单有效的机制,用来设置在响应刺激时执行的动作。在我们应用的初步修订中,我们引入了 Cloud Functions。利用模板代码,我们能够将系统适配为消费 RSS feed,并将内容输出到应用程序日志中。

在示例代码中,我使用了 BBC RSS Feed 网站作为源材料,因为它有很多选项,且是一个足够稳定的网站,可以作为演示的基础。然而,尽管如此,你可以随意替换这个站点,只要它遵循相同的标准规范,这不应影响所展示的代码。

做得很好:在 Google Cloud 上工作提供了很多选择。在下一节中,我将给你一个可选挑战,帮助你在此基础上构建你到目前为止学到的所有内容。

应用程序版本 5 – 扩展 RSS 阅读器

在这个应用程序的最终版本中,我将给你一个挑战!我们的应用程序已经可以获取 RSS 源数据并以 HTML 格式显示。然而,如果能够展示更多信息,甚至可能包括另外一个或两个源,那就更好了。

通过扩展需求,我们现在有了以下任务列表:

Ref 需求 描述
1 开发一个云函数。 部署一个单一功能的应用程序。
2 从 RSS 网站读取内容。 例如,BBC 的 RSS 源。
3 将数据呈现为 HTML。 将信息以 HTML 格式呈现。
4 刷新数据。 使数据能够在自动化计划下进行刷新。
5 从 RSS 网站读取内容。 查找另一个 RSS 网站并将其添加到应用程序中。
6 改进 HTML 布局。 向输出中添加一些图片,并从 RSS 源中添加更多数据。

大部分代码你应该已经非常熟悉,但如果不熟悉也不用担心。这个挑战应用的目的是测试你的理解力并建立你的信心,让你能够应对越来越复杂的代码。

记住,如果你发现这个练习有挑战性,解决方案可以在书籍的代码库中的 ch03/solution 子目录下找到。

在这个例子中,值得注意的是,构建一个单一功能的应用程序可以被视为无服务器函数的反模式。另一种方法可能是创建一个用于数据访问的读取函数,以及一个用于数据渲染的展示函数。这可能会导致状态迁移到其他服务,例如 Firestore(甚至只是保存到 GCS)。此外,添加一个 Cloud Scheduled 任务可能用于刷新数据。刷新后的数据可以存储在后端,如 Firestore 中。然后,我们可以使用写入触发器来触发事件驱动函数,渲染更新后的 HTML,甚至可能将数据写入 GCS 或其他缓存。采用这种方法,在没有获取 RSS 源的延迟情况下,我们可以为成千上万的前端请求提供服务,同时将工作负载解耦成小而易懂的代码。

总结

在本章中,你将会意识到,在 Google Cloud 上构建解决方案并不需要大量的知识就能达到一个最小可行产品MVP)。投入时间让自己能够快速迭代多个版本是值得的,并且这能带来整体设计上的显著改进。

通过本章的学习,您现在应该对可用服务的性质有所了解。您会发现,将不同的产品组合在一起,几乎像积木一样,变得更加容易。在这方面,最初清晰地了解最终解决方案的样子至关重要。借此机会构建轻量级函数,而不是单体架构,通常可以简化整体设计;但一定要确保这是您的解决方案所需要的。如您现在所知,有许多场景下,无服务器事件处理是非常有益的。

这就结束了我们对三种无服务器工作负载事件处理服务的初步概述。所讨论的功能的多功能性满足了各种不同的使用场景。在设计应用程序时,值得考虑预期的使用方式。设计迭代是件好事,应该能带来更好的效率。随着我们进入更复杂的系统,了解适当的使用场景和相关设计模式会让构建系统变得更加自然。在下一章中,我们将专注于云函数。使用方面还有很多内容需要涵盖,我们还需要讨论使它们如此有用的触发事件。

问题

  1. 云任务是否支持速率限制?(是或否)

  2. 描述云任务的延迟调度。

  3. 云调度器是否支持重试和退避机制?(是或否)

  4. 云调度器的工作方式是否与 cron 类似?(是或否)

  5. 我们使用什么触发器来部署 HTTP 端点?

  6. 云函数的日志发送到哪里?

  7. 什么命令可以启用从命令行部署云函数?

  8. 云函数的入口点是什么,为什么它很重要?

进一步阅读

第四章:开发 Cloud Functions

在本章中,我们将提供 Google Cloud Functions 的概述。它将帮助您深入了解 Google Cloud Functions 的背景、原因及其工作原理。了解任何技术及其用例的基础将有助于在现实世界中进行集成和应用。

为了实现这一目标,我们将通过本章节构建一个应用程序,以展示 Cloud Functions 的一些关键方面。

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

  • 介绍 Google Cloud Functions

  • 使用 Functions Framework 进行开发

  • 探索 Cloud Functions 的工作流程

  • 理解 HTTP 协议

技术要求

在本章中,要完成练习,您将需要一个 Google Cloud 项目或一个 Qwiklabs 账户。

您可以在 GitHub 书籍存储库的ch04子目录中找到本章的代码文件,链接为github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch04

当您阅读本书中的代码片段时,您会注意到在一些情况下,代码/输出的几行已被删除并替换为省略号(...)。省略号的使用是为了只展示相关的代码/输出。完整的代码可在上述链接的 GitHub 上找到。

介绍 Google Cloud Functions

Google Cloud 上关于 Cloud Functions 的描述表明,这是一个事件驱动的无服务器计算平台。这意味着函数通过 HTTP 端点或通过后台服务(例如 Google Cloud Storage 或 Cloud Pub/Sub 以及 Google Cloud 内的其他源)触发。在操作上,Cloud Functions 是单次使用的代码片段,部署迅速,并在多个服务之间提供粘合。Cloud Functions 的有趣之处在于,它们可以像传统应用程序一样轻松地进行组合。例如,完全可以创建几个 HTTP 端点函数,链接到 Cloud Pub/Sub 后端,通过保持服务简单,可以快速完成构建。

虽然函数的默认设置是公开的,但可以为功能启用身份验证以确保其运行环境的安全性。Cloud Functions 使用服务账号执行,并可通过 Google Cloud 身份和访问管理IAM)进行配置。需要注意的是,函数不共享内存空间,这意味着每个执行实例都处于隔离的上下文中。

此外,需要注意的是函数具有无状态性质;因此,不应期望在 Cloud Function 中保留信息。

这个场景的例外情况是像持久化数据库连接这样的操作,这些连接应该在代码中全局存储。这样做意味着下次调用云函数时,不需要引入额外的延迟来获取连接信息。

此外,以下属性是云函数 DNA 的核心:

  • 它们在安全的操作环境中运行。

  • 它们在函数之间不共享内存。

  • 没有状态保持。

使用云函数提供了一种极好的方式来执行单一目的的云函数,而无需耗费大量时间编写应用程序代码。更多的使用场景将在以下部分中介绍。

定义使用场景

随着开发人员对云基础设施的了解加深,一个令人兴奋的结果是轻量级函数的演变。想想你每天使用的应用程序,它们是如何随着时间的推移而发展的。现在有多少个应用程序带有可以独立于应用程序使用的 HTTP 端点?如果一切都变成 API,那会怎样?这就像日志处理的情况,记录曾经以专有格式保存在内部。后来有人想到将应用程序数据导出到集中式解决方案来捕获信息。出现了一个转折点,我们都接受了这个典型的模式,成为了日志管理的事实标准。

回到云函数—考虑一下,当你拥有可以轻松集成的标准解决方案和模式时,开发一个应用程序会变得多么方便。同时,考虑一下为了确保应用程序保持一定的兼容性,前期需要设计多少内容。使用像 HTTP 这样的协议提供了一个广泛被认可的、易于理解的接口。拥有一个典型的参考框架为推动行业发展提供了极好的方法,而 HTTP 恰恰是推动抽象接口开发的载体。因此,在接下来的几节中,我们将讨论云函数最适合的使用场景。

处理后端服务

后端无服务器计算由像 Cloud Pub/Sub 和 Cloud Storage 这样的服务触发。这些服务的范围因能够将不同的 Google Cloud 服务与云函数集成而变得更加有趣。

我们已经知道可以在许多场景中使用 Cloud Functions。如前文的用例介绍所述,无服务器后端处理是这个解决方案的一个优秀应用场景。在这种情况下,Cloud Function 的设计旨在处理请求并提供特定信息。此请求展示了服务的特定签名,封装了完成处理一些任意数据所需的信息。在处理开始时,函数会响应并指示其已完成分配的任务。响应可以标明成功或失败;然而,就服务而言,它的生命周期已经结束,且将不再存在。

处理实时数据

另一个非常适合无服务器计算的场景是实时数据处理。在这种情况下,Cloud Functions 的按需提供能力使其能够满足设计要求较低延迟的服务的处理需求。正如我们在本书前面所讨论的,理解几个关键属性可能对开发解决方案非常有帮助。能够在短时间内快速提供基础设施是需要实时处理的解决方案中的一个重要因素。Cloud Functions 支持在内存分配小于 1,024 MB 的情况下启动时间少于 2.5 秒,这在需要立即处理的应用程序中尤为有益。随着许多系统从批处理转向近实时处理,采用这些能力的情况将变得越来越普遍。因此,理解如何在设计的服务中适应并结合这些模式变得越来越重要。

除了前面章节中提到的示例场景外,还有许多其他的用例。使用 Cloud Functions 可以最大限度地减少集成代码所需的工作,并且消除了提供相关基础设施的义务。

使用 Cloud Functions 几乎不需要额外了解后端架构。事实上,只要你熟悉某种语言运行时环境,就可以非常快速地开始使用。尽管如此,我们仍然应该花时间解释一下后台发生了什么,以及如何扩展这些功能以满足你的需求。

在接下来的章节中,我们将继续讨论函数框架。Google Cloud 极力确保开发者能够获得优秀的体验。实现这一目标通常意味着允许开发者在他们现有的环境中工作。那么,如何在本地工作同时又能够将代码部署到云环境中呢?

使用函数框架进行开发

此时,请花点时间考虑我们一直在讨论的 Functions Framework 的好处——Google Cloud 上的各种选项,意味着我们可以在不同的环境中运行代码。作为一个以开发者为中心的平台,允许工程师在不同的上下文中使用他们的代码是非常有利的。

介绍 Functions Framework

Functions Framework 是一个开源项目,可以让您在多种环境中构建函数,例如本地环境。就像任何魔术一样,美丽之处不在于理解幻觉的机制,而是它让你感受到的方式。在这个例子中,让我们拉开帷幕看看这个特定魔术是如何完成的,以及在 Cloud Functions 方面意味着什么。

那么为什么我们需要类似这样的东西呢?嗯,在本地工作,通常是高度定制化的环境,为开发者提供了友好的舒适区。您工作的上下文将会是熟悉的,并且工具、位置和访问都可以轻松检查和验证。在这种方式下工作具有固有的舒适感。转向云端会从用户那里移除一些心理上的舒适感。

尽管与在云中运行代码的最终目标相悖,但具备在本地工作的能力是开发人员想要做的事情。能够使用 Cloud Functions 在多种环境中工作将我们带到了 Functions Framework。这个框架是一个轻量级的依赖项,允许开发人员在一系列设置中运行简单的接口,例如以下设置:

  • 云函数

  • 本地开发

  • Cloud Run 和 Cloud Run for Anthos

  • Knative 环境

建立在函数即服务FaaS)平台上为 Google Cloud,此框架的可用性提供了跨多个环境实现可移植性的简单机制。要将代码整合到项目中,只需添加相关的包。在接下来的部分,我们将通过一个简单的示例来演示其在项目中的使用。在下面的案例中,我们创建一个基本的 Node 应用程序来处理一些基于 web 的信息。

在本地部署应用程序使用 Functions Framework

大多数开发人员更喜欢在开发环境中工作的本地体验。Functions Framework 为 Cloud Functions 开发人员提供了在本地工作环境中继续使用这种能力,并无缝地将其代码传播到云端的能力:

  1. 首先,为要构建的示例代码创建一个目录并初始化环境。在我的示例中,我创建了一个名为functions-framework的目录,并将其用于开发我的代码。

需要注意的是,在我的本地环境中,我正在使用 Node v10+,这是使用 Functions Framework 的要求。此外,我在测试机上安装了 npm v6.9.0。

  1. 要检查您环境中的 Node 版本,请运行以下命令:
node -v
  1. 使用编辑器创建一个名为index.js的文件,并添加以下内容:
exports.helloWorldLocal = (req, res) => {
    res.send('Hello, Functions Framework from a local machine');
};
  1. 要创建一个模板package.json文件,从命令行运行npm init。根据需要编辑内容:
{
    "name": "lab01_FF",
    "version": "1.0.0",
    "description": "Example use of the Functions Framework (FaaS)",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "Rich Rose",
    "license": "ISC"
}

如上面的代码片段所示,我已经在我的文件中添加了最基本的内容。

  1. 接下来,安装functions-framework包。我假设这个包尚未安装在你的本地机器上。要安装此包,你将使用npm来获取必要的包信息:
npm install @google-cloud/functions-framework
  1. 完成此命令后,你会看到package.json文件已经更新,包含对 Functions Framework 的引用,位于dependencies类别下,如下所示:
{
    "name": "lab01_FF",
    "version": "1.0.0",
    "description": "Example use of the Functions Framework (FaaS)",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "Rich Rose",
    "license": "ISC",
    "dependencies": {
        "@google-cloud/functions-framework": "¹.1.1"
    }
}

现在我们的环境已经准备好构建函数了,接下来让我们创建一个非常简单的应用程序来测试我们的环境。

  1. 通过我们新的代码,我们需要告诉应用程序在运行时执行一个脚本。在现有的package.json中,添加以下代码来调用 Functions Framework 并传递一个target函数:
{
    "name": "lab01_FF",
    "version": "1.0.0",
    "description": "Example use of the Functions Framework (FaaS)",
    "main": "index.js",
    "scripts": {
        "start": "functions-framework --target=helloWorldLocal"
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "Rich Rose",
    "license": "ISC",
    "dependencies": {
        "@google-cloud/functions-framework": "¹.1.1"
    }
}

做得好;现在我们已经准备好使用 Functions Framework 的index.jspackage.json文件。

  1. 使用标准的调用方法从命令行运行应用程序npm start,本地运行应用程序。

  2. 注意,在我们的示例环境中,运行的应用程序绑定到 HTTP 协议上的端口8080。打开浏览器并访问此 URL http://localhost:8080,以查看应用程序的输出,截图如下所示:

使用这个框架,在开发机器上运行本地代码是微不足道的。然而,如果我们想要部署到云端,这种场景会有什么不同呢?在下一节中,我们将探讨如何将代码部署到云端。

使用 Functions Framework 将应用程序部署到云端

当将本地应用程序迁移到云环境时,令人惊讶的是,在准备工作方面,我们需要做的事情很少。在这个例子中,我假设你已经安装并初始化了 Google Cloud SDK,以便你能够连接和配置 Google Cloud 上的资源。

为了更清晰地说明,我将创建另一个说法稍有不同的函数;不过,你可以使用之前代码中定义的相同云函数:

  1. index.js文件中,创建一个名为helloWorldCloud的第二个函数。

  2. 给这个新函数赋予之前定义的属性;不过,我们将使用不同的信息来说明调用的是不同的函数:

exports.helloWorldCloud = (req, res) => {
    res.send('Hello, Functions Framework from a Google Cloud project');
};
  1. 保存新应用程序的内容并进行测试,确保它像上次运行时一样正常工作。

  2. 从命令行运行npm start。记住,我们在这个版本中唯一更改的是代码库。

  3. 运行npm start命令后,我们可以浏览到localhost:8080并看到来自我们应用程序的信息。

  1. 现在,我们希望通过部署云函数在云端运行我们的代码库。在这个实例中,我们依赖 Cloud SDK 将我们的代码运行在 Google Cloud 项目中。运行以下命令:
gcloud functions deploy helloWorldCloud --runtime nodejs10 --trigger-http --region us-central1
  1. 为了确认上传的源信息是否成功创建,请查看正在运行的云函数源代码。这里,我们可以看到它包含了与上传到 Google Cloud 的已归档源代码相同的内容。然而,显示结果中显示的是与我们新函数相关的消息。

到目前为止,我们已经了解了如何将代码部署到本地环境,并使用云函数将其部署到 Google Cloud 项目中。但云函数究竟是做什么的,它能做什么呢?在下一节中,我们将进一步探讨该服务的相关好处和使用场景。

探索云函数工作流

所以,看来云函数非常有用,并且与函数框架结合使用时,似乎是一对天作之合。而且,这种整体方法看起来非常具有扩展性,与 Google Cloud 内的其他产品和服务协作也变得大大简化。所以,如果你已经在Google Cloud 团队中,使用云函数显然是个明智的选择。

对于那些不在Google Cloud 团队或可能持观望态度的朋友,这背后还有更多的故事。无服务器(Serverless)是一个通用术语,多年来曾受到一些批评。对大多数人来说,无服务器意味着部署代码时无需管理基础设施。对其他人来说,这意味着服务能立即可用,并能够根据应用需求进行合适的调整,并具备自愈能力。

根据你的角度,云函数可能满足或不满足你的需求。人们常提到的主要问题是部署代码所花费的时间。为什么函数不能立即可用?为什么内存受到限制?,等等——当我使用供应商 X 时,服务能立即使用,内存没有限制,而且每次部署时他们还会提供茶水和蛋糕!

在深入了解云函数之前,现在似乎是个不错的时机来讨论一下在我们将代码部署到云端时,幕后发生了什么。Google 为数据处理管道的关键产品绘制了一张出色的架构图。数据生命周期从架构的角度来看非常有用,它概述了能够呈现管道中特定方面的产品。就数据摄取、存储、处理和分析这些常见主题而言,这种方法提供了一种令人兴奋的方式来分解组件架构。

那么,如何创建一个函数呢?让我们在下一节中详细探讨。

云函数属性

如前所述,Cloud Functions 支持 Google Cloud 上的多种运行时,包括 Node.js、Python、Go、Java 和 Ruby。开始使用 Cloud Functions 如同访问 Google Cloud 控制台并选择最合适的运行时一样简单。每个运行时都包括模板代码,提供了一个主要 Cloud Function 的蓝图以及运行该示例所需的所有辅助系统代码。

代码的一个关键考虑因素是算法处理所需的时间以及这一点有多重要。例如,如果算法是用户体验的一部分,那么任何处理延迟都可能会被最终用户察觉。异步代码允许将长期的处理操作从主线程中卸载并在后台执行。采取此行动对于保持整个应用程序性能至关重要,因为它提供了在需要快速响应和缓慢响应的资源之间进行区分的能力。

此外,正如你现在所知道的,函数通常是无状态的应用程序,不应持久化任何信息。作为一般规则,这意味着该服务采用一个短生命周期,其中数据处理和任务完成。在代码中使用异步调用来减少长期的请求和响应周期,能够减少潜在的延迟。

对于有状态的应用程序,维护信息的需求在数据持久化相关的潜在延迟方面提出了问题。应该清楚,任何需要读/写周期的操作,如果不在内存中,将会引入一定的延迟。尽量减少这种延迟提出了一个有趣的设计问题,最终取决于你愿意放弃什么,以及你希望在性能、松耦合、数据设计等方面走多远。

要调用函数,Google Cloud 上的事件通知机制是触发器。触发器是开始开发 Cloud Function 时需要考虑的重要因素。函数设计确实需要一些思考,以确定需要哪些组件来交付一个合适的模型。作为事件驱动架构,提供了一些可用的选项,如下表所示:

触发器 事件 目标
HTTP HTTP URL
Cloud Pub/Sub 主题
Cloud 存储
  • 完成/创建

  • 删除

  • 存档

  • 元数据

存储桶
Cloud Firestore
  • 写入

  • 创建

  • 更新

  • 删除

Google Analytics for Firebase
Firebase 身份验证
  • 创建

  • 删除

Firebase 实时数据库
  • 写入

  • 创建

  • 更新

  • 删除

数据库
Firebase 远程配置

除了运行时之外,在创建 Cloud Function 时还包括其他一些属性。内存分配是一个重要的考虑因素,因为预期的处理是在内存中进行的。Cloud Functions 提供从 128 KB 到 2 GB 的内存分配,开发者可以根据应用程序的需求进行调整。

在进入更一般的选项之前,值得简要了解身份验证及其与 Cloud Functions 的关系。

身份验证

Cloud Functions 的默认身份验证代表公共访问;也就是说,任何人都可以访问。从 Cloud Functions 的配置屏幕中,选择允许未经身份验证的调用以创建一个公开暴露的端点。allUsers的 IAM 设置为与 Cloud Function 相关联的服务账户提供公共访问权限。函数的调用依赖于服务账户的关联。服务账户是一个用于非交互式访问的特定账户,就像入口是为另一台计算机而不是人类设计的。用户账户被分配角色权限,并绑定到服务上。默认情况下,Cloud Functions 绑定了allUser权限,但在函数创建之前或之后可以进行更改。

请注意,初始的功能指定允许通过 IAM 设置进行调整。这种方法使 Cloud Functions 与 Google Cloud 上的其他服务保持一致。本书后续将讨论 Cloud Functions 的安全性,并涉及与 Cloud Functions 相关的服务账户配置,以限制权限。

现在,让我们继续讨论有关访问函数源版本的选项。

源代码管理

使用源代码时会面临许多挑战。幸运的是,Cloud Functions 提供了几种集成源代码的选项。可以通过以下来源访问 Cloud Functions 的源代码:

  • 内联编辑器:在 Google Cloud 控制台中创建和修改函数代码。

  • ZIP 上传:将本地 ZIP 文件上传到中介的 Cloud Storage 存储桶。

  • 来自 Cloud Storage 的 ZIP:使用位于 Cloud Storage 存储桶中的现有 ZIP 文件。

  • Cloud Source 仓库:使用源代码版本控制。

对于快速开发,内联编辑器非常有用;然而,在编写更复杂的代码时,使用其他多种选项可能更有意义。在大多数情况下,您可以继续使用自己喜欢的编辑器,并通过符合需求的任何方法上传代码。

了解如何上传源代码之后,我们可以顺利进入下一个话题,即选择语言运行时。

使用不同的运行时语言

在接下来的几段中,我们将概述使用 Node、Python 和 Go 的基本函数,并讨论各种运行时语言之间的高层次差异。

Node.js

我们的讨论从 Node.js(v10)开始。在这个模板代码中,我们可以看到实际的函数由几行代码组成。就 Node.js 而言,使用的具体框架是 Express.js。Express.js 是一个简洁的框架,与 Node.js 配合使用,通过增加如路由中间件和 HTML 渲染等功能来增强其功能。

在下面的示例代码中,我们可以看到一个基于 exports.helloWorld 的函数定义。exports 关键字表示该函数会在部署后执行(即,作为可调用函数暴露)。还要注意,待导出的服务需要两个参数,分别映射到 HTTP 请求和响应的值。

默认的 Google Cloud Function(用 Node.js 编写)如下所示:

1\. /**
2\. * Responds to any HTTP request.
3\. *
4\. * @param {!express:Request} req HTTP request context.
5\. * @param {!express:Response} res HTTP response context.
6\. */
7\. exports.helloWorld = (req, res) => {
8\.     let message = req.query.message || req.body.message || 'Hello World!';
9\.     res.status(200).send(message);
10.};

函数体检查所做的请求,以确定是否添加了查询消息或消息体。

查阅 Express.js 文档让我们了解更多关于消息赋值语句的访问要求。对于 GET 消息,我们了解到查询可以将附加数据传递给函数。

如果没有,则会返回默认的 Hello World 消息到消息变量。接下来,res.status 被设置为 200 的值,并将消息变量添加到响应中。记得在前面讨论的 HTTP 响应码中,200 表示交易成功。

Python

目前在 Google Cloud 上支持的 Python 版本是 3.7。从示例代码中我们看不到的是 Flask 包处理了 HTTP 通信。一眼看上去,Node.js 示例中显然包含了比这更多的代码。

首先,我们定义我们的函数,并且可以看到命名约定的一个明显不同之处。在 Python 中,我们将服务命名为 hello_world,这与 Node.js 示例中的命名不同。你可以自由更改名称以保持一致,但这并不影响最终结果。确保你使用正确的函数名称执行,因为这是一个可能很难修复的错误:

def hello_world(request):
""" Responds to any HTTP request.
    Args:
        request (flask.Request): HTTP request object.
        Returns:
    The response text or any set of values that can be turned into a
    Response object using
    `make_response
    <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>`.
"""
request_json = request.get_json()
if request.args and 'message' in request.args:
    return request.args.get('message')
elif request_json and 'message' in request_json:
    return request_json['message']
else:
    return f'Hello World!'

在函数体内,我们可以看到调用返回一个 JSON 对象。类似于 Node.js Cloud Function,我们检查传递的消息参数,看看是否要覆盖默认消息。为了确保兼容性,我们再次确保这个示例适用于 GET 和 POST 消息。最后,如果用户没有按要求格式添加消息,我们将返回默认的 Hello World! 消息。

Go

Go 目前在 Google Cloud 上支持版本 1.11 及以上。如你所料,每个版本发布时都会保持修订的兼容性,因此建议保持与新版本的兼容性。

在下面的示例代码中,我们导入了一些包,以使我们的应用程序能够执行必要的 HTTP 和 JSON 访问。函数签名保持不变,使用请求和响应参数,作为函数的入口。

对于函数体,如果程序未提供信息,我们的代码将默认显示标准消息:

// Package p contains an HTTP Cloud Function.
package p
    import (
        "encoding/json"
        "fmt"
        "html"
    "net/http"
)
// HelloWorld prints the JSON encoded "message" field in the body
// of the request or "Hello, World!" if there isn't one.
func HelloWorld(w http.ResponseWriter, r *http.Request) {
    var d struct {
        Message string 'json:"message"'
    }
    if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
        fmt.Fprint(w, "Hello World!")
        return
    }
    if d.Message == "" {
        fmt.Fprint(w, "Hello World!")
        return
    }
    fmt.Fprint(w, html.EscapeString(d.Message))
}

使用 Cloud Functions,我们看到,无论选择哪种运行时语言,许多属性仍然是共享的。在语言方面,我们还观察了用于 Node、Python 和 Go 的不同形式的示例代码。

在编写我们的函数时,寻找设计模式将帮助我们避免重复造轮子。我们了解了更多关于可用语言运行时的信息,并看到了与每个运行时相关的样板代码示例。要测试该函数,我们需要一个触发事件,在下一节中,我们将讨论这种情况的处理方法。

测试运行时触发事件

为了测试函数,我们可以创建一些数据来确认我们服务的成功标准。同样,Cloud Functions 可以通过允许在测试窗口中直接传递 JSON 数据给函数来帮助完成这项任务:

  1. 云函数的典型 URL 将类似于以下链接的语法。访问端点将访问与 Cloud Function 关联的默认信息:
https://[REGION]-[PROJECT].cloudfunctions.net/[CLOUD-FUNCTION]
  1. 对于模板代码,我们可以通过添加一些额外信息到 URL 来替换新消息,具体如下:
https://[REGION]-[PROJECT].cloudfunctions.net/[CLOUD-FUNCTION]?message=Yo%20Gabba %20Gabba!

在此示例中,req.query属性返回一个包含信息的对象,本例中是要显示的消息,代替默认的Hello World!

  1. 如果您继续执行此函数而不输入任何额外的参数,则将显示Hello World消息,并生成200响应代码。通过这种方式触发事件类似于直接访问函数 URL,因此不会将任何其他信息合并到查询中:
消息 触发事件 输出 响应返回码
1 {} Hello World! 200
  1. 或者,通过添加一些参数到触发事件,运行函数时可以输出特定的字符串。
消息 触发事件 输出 响应返回码
2 {"message":"Yo Gabba Gabba!"} Yo Gabba Gabba! 200

在上述示例中显示的触发事件中添加了一个 JSON 字符串。在这种情况下,函数的输出将更改以显示之前输入的字符串。与之前一样,生成的返回代码是200

能够测试应用程序对任何开发者工作流程都是基本的。然而,同样重要的是能够引用日志和应用程序特定信息以进行监控。在 Google Cloud 中,我们使用 Stackdriver 来实现这一点。

Stackdriver

每次流量到达终端时,关于云函数的信息会自动在 Stackdriver 中捕获。如果你不熟悉 Stackdriver,它是 Google Cloud 的集中式日志记录和监控系统。我们将在本书的第十章“Cloud Run 实验室”中深入讨论 Stackdriver;然而,当前要知道的是,这里是访问函数实时信息的地方。要搜索函数执行,可以在 Stackdriver 中使用过滤器,如下所示:

字段 示例内容
resource.type cloud_function
resource.labels.function_name function-1
resource.labels.region us-central1
labels.execution_id 4ubpvdgz7c8w

选择使用哪种运行时语言的决定因素在很大程度上取决于你个人的经验。我在本书中使用了 Node.js,因为它非常易于上手。它的界面简单高效,理解代码不需要太多思考。当然,你可能有自己的最爱,且希望在那些运行时语言中也有类似的包可用。

使用模板代码是你旅程的开始,所以不要害怕尝试不同的方法和技巧。在接下来的部分,我们将概述 HTTP 协议。

理解 HTTP 协议

在我们讨论云函数的背景下,理解 HTTP 协议的工作原理至关重要——学习 API 的基础,以便实现请求与响应之间的通信。

HTTP 动词,如 GET、PUT、POST、DELETE 等,为 HTTP 协议执行其各种功能提供了基础。这种契约的设计经过了时间的考验,随着更高级用例的出现,它变得越来越普及。通常运行在 TCP 上,该协议需要一个可靠的媒介来传输消息——利用像 TCP 这样的契约为数据传输和机器间通信时的确认建立了一定的弹性。在常见的使用方法中,GET 和 POST 是应用中最常用的方法。

在接下来的部分,我们将简要概述这些方法,以帮助理解它们的使用。

定义 GET/POST 方法

访问网页通常使用 GET 方法来获取存储在远程服务器上的内容。每当你浏览互联网并查看各种网站的内容时,HTTP GET 方法都会被用来实现这一点。GET 方法在最基本的形式上是 HTTP 的幂等获取机制。将 GET 方法看作一个简单的获取机制,也就是从远程服务器获取这些信息。向远程服务器发送这个命令,它会提供一个消息响应。

GET 方法也允许将查询信息发送到远程服务器;然而,这些信息会显示在 URL 中。消息负载通常以字节流的形式提供,之后远程服务器会关闭开放的 TCP 连接,表示事务已完成。

在处理网页时,除了使用 GET 方法从网站获取信息外,还有 POST 方法。POST 方法提供了将额外信息作为查询的一部分发送到远程服务器的能力。与 GET 方法不同,查询信息不会通过 URL 暴露,并且请求是非幂等的。这些特性使得 POST 方法更适合用于发送不应通过 URL 曝露的内容,并且可能发生变化的信息。这类方式的典型应用是 Web 表单,其中字段值作为 HTTP 请求查询的一部分发送。

一旦请求/响应周期完成,系统将返回一个表示事务状态的状态码。在接下来的主题中,HTTP 代码的概述将概述这些常见类别。

解码 HTTP 响应码

一个 HTTP 响应的三位数响应码表示通信是否成功。这些响应码对于确保任何基于 HTTP 的应用服务器的顺利运行至关重要。

如果你曾经需要配置或维护一个 Web 服务器,以下信息对你来说无疑会非常熟悉:

返回值 类型 描述
1xx 信息 一个信息性响应码,表示请求已被理解。
2xx 成功 确认请求成功的最常见方式是返回此状态码。
3xx 重定向 与客户端请求相关的重定向;通常不需要用户交互,因为重定向会自动执行以完成操作。
4xx 客户端错误 基于请求的错误,表示客户端发送到服务器的信息有问题。
5xx 服务器错误 与服务器端通信响应相关的错误,表示请求无法完成。

HTTP 状态码的完整列表由互联网编号分配局IANA)维护。与 HTTP 状态码相关的广泛类别确保在处理此领域时能涵盖大多数情况。

另一个可能对扩展理解有帮助的领域是如何跨域处理网页内容。在接下来的章节中,我们将简要概述跨域资源共享(CORS)。

使用跨域资源共享(CORS)

在 HTTP 头中启用 CORS 可以实现跨域执行一系列操作。添加这个功能提供了一种超越传统单域客户端与服务器信息交换的额外能力。正如你所预期的,客户端的责任是发起请求,并在请求中指明方法和头部要求。从服务器的角度来看,头部属性的范围更多,允许实现更丰富的功能集。

一个应用可能并不总是在同一域中执行。在这种情况下,CORS 提供了跨域通信的能力:

客户端 服务器

|

  • 起源

  • Access-Control-Request-Method

  • Access-Control-Request-Headers

|

  • Access-Control-Allow-Origin

  • Access-Control-Allow-Credentials

  • Access-Control-Expose-Headers

  • Access-Control-Max-Age

  • Access-Control-Allow-Methods

  • Access-Control-Allow-Headers

|

最后,我们概述了与 Cloud Functions 相关的事件驱动触发器。通过查看这些选项,可以清楚地看到,这些触发器能够满足许多构建系统时相关的用例。将事件和触发器视为胶水乐高积木是一个很好的比喻,能够帮助理解这些组件的使用。当涉及到构建新组件或设计无服务器应用时,通常令人惊讶的是这些构建块能如此轻松地协同工作。但事情并不止于此——想象一下,超越 Google Cloud,使用其他云服务商的服务。无服务器为现有设计和应用开发过程带来了真正激动人心的变革。专注于提供清晰接口并具有明确输入和输出的单次使用功能,可以提高交付和维护效率。

逐一讨论了每个选项时,我们讨论了如何在构建无服务器应用时充分发挥其重要作用。为了实现这一点,一种适应性的架构不仅仅需要云函数(例如,在集成认证(通过 Firebase)、代理访问(通过 Cloud Endpoints)或临时存储(通过 Google Cloud Storage)时),还需要更多的东西。遵循最佳实践并结合不同的方法并非“一刀切”的事宜,你能够无缝地使用开源和商业软件,超越无服务器平台,关注系统的更广泛方面。虽然无服务器提供了一种实现扩展的简便方法,但在你其他部分引入网络扩展时,仍然需要仔细规划。

总结

在本章中,我们概述了 Cloud Functions 的整体情况,并讨论了它在 Google Cloud 无服务器产品中的位置。此外,我们还查看了几个与使用 FaaS 相关的边缘话题,例如 HTTP2 和数据生命周期。

通过探索运行时,并观察整章关联的代码,我们现在知道如何在多种语言中构建云函数。我们还了解到通过将监控和日志记录纳入开发工作流程,如何测试和解决问题。正如您所知,Google 提供了一整套工具和服务,让您能够迅速开发最小可行产品MVP)。

在下一章中,我们将更深入地讨论一些示例。构建应用程序是学习无服务器应用程序环境关键要素的绝佳方式。

问题

  1. Cloud Functions 运行在哪个端口?

  2. Cloud Pub/Sub 使用的是哪种触发器?

  3. Google Cloud Functions 支持哪些运行时语言?

  4. 成功的 HTTP 响应代码是什么?

  5. 客户端错误的 HTTP 响应代码是什么?

  6. 服务器端错误的 HTTP 响应代码是什么?

  7. CORS 的目的是什么?

  8. 如何启用 CORS?

进一步阅读

第五章:探索函数即服务(Functions as a Service)

在本章中,我们将深入研究 Google Cloud 上的 Cloud Functions。我们已经涵盖了相当一部分内容;然而,还有很多知识需要了解和学习。到目前为止,我们的主要关注点是理解 HTTP 端点,并构建一些简单的应用程序,以展示其相关功能。除了令人兴奋的 HTTP 事件功能外,还有后台功能,即那些不需要访问外部 HTTP 端点的功能。

为了加深我们对这些类型函数的理解,我们将在本章中构建几个工具,以说明各种概念和技术。我们将继续利用 Functions 框架创建代码,并开始集成外部系统,以展示构建满足我们需求的工具的简便性。

在本章稍后,我们将基于创建SignedURL函数,利用 Google API 来构建一个简单的应用程序,提供一种建立时间限制 URL 的方式。源数据将存储在 Cloud Storage 上,我们将扩展我们的函数,添加一个简单的前端。最后,我们将继续使用 Functions 框架,使我们能够在本地工作并保持与 Google Cloud Functions 的兼容性。

在本章中,我们将学习以下主题:

  • 开发 HTTP 端点应用程序

  • 探索 Cloud Functions 和 Google API

  • 探索 Google Cloud Storage 事件

  • 构建增强的签名 URL 服务

技术要求

为了完成本章的练习,你需要一个 Google Cloud 项目或一个 Qwiklabs 账户。

你可以在书籍的 GitHub 代码库中找到本章的代码文件,位于ch05子目录下,链接为github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch05

当你在书中浏览代码片段时,你会注意到在一些情况下,代码/输出中的几行已被移除,并用省略号(...)替代。使用省略号仅是为了显示相关的代码/输出。完整代码可以在 GitHub 上找到,链接已在前文提到。

开发 HTTP 端点应用程序

使用 Cloud Functions 可以让独立和孤立的组件创建扩展功能。这些组件或微服务提供了一个出色的方式来构建解耦的架构。在这个例子中,我们将回到基础,学习如何扩展我们的知识,以调用 Google Cloud API。

事件提供了响应与提供者相关的系统通知的能力。如前几章关于 Google Cloud 所述,这些提供者通过定义的提供者接口(如 Cloud Pub/Sub 和 Cloud Storage)呈现多个扩展服务的选项。

我们已经看过通过 URL 调用的 HTTP 函数。利用相同的语义标记(例如,GET/POST)和签名(例如,请求/响应)进行 HTTP 通信,这些类型的函数已经得到广泛理解,并可以在现有知识的基础上构建。由于 HTTP 的复杂性随着时间的推移逐渐抽象化,HTTP 构造的普遍理解代表了一个被广泛理解的 API。

然而,并非所有内容都支持 HTTP 端点;因此,需要另一种将提供者与现有服务集成的方法。后台(即异步)功能扩展了 Cloud Functions 模型,通过参数建立中间件接口,能够在不同组件之间传递数据。这类提供者的示例包括 Cloud Pub/Sub 和 Cloud Storage,它们都提供了丰富的消息传递和事件通知接口。在这两种情况下,任何呈现的数据都必须符合支持的标准模式,才能响应提供者接口。

此外,还需要定义触发器以调用后台 Cloud Functions。然而,集成机制会根据使用的触发器而变化。在接下来的章节中,我们将重点讨论 Google Cloud 上可用的不同类型的触发器。对于后台功能,Cloud Pub/Sub 和 Cloud Storage 将是主要讨论的领域;然而,还有许多其他触发器类型可供应用程序使用。

触发 Cloud Pub/Sub

Cloud Pub/Sub 触发器基于消息队列,在该队列中信息在发布者和订阅者之间传递:

  • 发布者 负责与主题一起传播消息的模式。

  • 主题 表示信息消费所在的可用队列。

  • 消息队列的 订阅者(消费者)可以读取队列中关联的信息。

值得指出的是,一般来说,订阅分为拉取订阅和推送订阅,如下图所示:

在前面的图表中,您可以看到拉取和推送订阅类型的概述。

对于需要高吞吐量的场景,当前拉取机制是管理此类需求的最有效方式。

发布者和订阅者共同工作,使消息队列的两端能够将有效载荷从源头传送到目标。Cloud Pub/Sub 使用多种场景来提供信息的双向传输,确保源和目标之间的一致性。如你所料,Cloud Pub/Sub 关联了许多设计模式,确保数据传输按照所需的服务计划定期进行。

消息的分发使用全球分布的消息总线,使得系统之间能够交换信息。在这种情况下,Cloud Pub/Sub 的事件系统使用推送机制与 Cloud Functions 触发消息。定义了 google.pubsub.topic.publish 的触发器类型,用于管理 Cloud Functions 使用的事件,使你能够完全控制事件的发布。每次发布消息时,事件通知会确定要发布的消息负载。

除此之外,Cloud Pub/Sub 支持根据你的需求不同的设计模式。订阅者会为每个处理过的消息提供确认。若订阅者在 ackDeadline 阈值内未能确认消息,则该消息负载会被重新发送。根据你的使用场景,以下模式是考虑将 Cloud Pub/Sub 纳入设计时的良好示例:

模式 描述
直通处理 简单的队列机制——从主题到订阅。
多个发布者 同一主题的多个发布者——这使得源消息可以并发处理。
多个订阅者 同一订阅的多个订阅者——这使你可以为通过订阅消费的原始主题设置不同的订阅者。

Cloud Pub/Sub 的事件具有定义的触发器值 google.pubsub.topic.publish。当有效负载消息发布到与 Cloud Function 关联的事件类型时,事件通知会被触发。输入数据将包含传递的消息负载,并执行指定的 Cloud Function,以将数据传递给任何设置为接收信息的订阅者。

以下示例说明了如何使用 Cloud Pub/Sub 和后台函数;展示的蓝图代码代表了 Google 使用的默认函数:

/**
* Triggered from a message on a Cloud Pub/Sub topic.
*
* @param {!Object} event Event payload.
* @param {!Object} context Metadata for the event.
*/
exports.helloPubSub = (event, context) => {
  const pubsubMessage = event.data;
  console.log(Buffer.from(pubsubMessage, 'base64').toString());
};

如你所见,背景函数定义的签名保持一致。我们定义了一个 exports 函数,触发器在此上定义了相应的操作。与事件信息相关的服务需要参数,即事件负载数据和事件元数据。在 Cloud Pub/Sub 的 Cloud Functions 示例中,该函数将获取消息队列的输入并将内容显示为日志条目。

创建 Cloud Pub/Sub 类型的事件触发器需要在函数初始化时创建一个主题。创建后,测试该服务展示的属性与我们之前看到的 HTTP 触发器相同,例如区域、内存分配、超时和最后的部署。然而,在这里,HTTP 端点被 Cloud Pub/Sub 的触发器类型及其关联的定义主题所替代。

测试该功能时,需要记住一个重要事项,那就是 Cloud Pub/Sub 期望其数据格式为 base64。请按照以下两个步骤进行操作:

  1. 将数据转换为base64以触发来自 Cloud Functions 测试页面的事件。幸运的是,你可以通过在命令行输入以下内容,在 Google Cloud Shell 中完成此操作:
base64 <<< "Rich"
  1. 前面命令的输出显示了输入文本的base64等效形式:

在前面的示例中,我们需要手动使用base64转换信息。如果你使用 Cloud Pub/Sub 菜单项或gcloud pubsub topics publish命令,这两个工具将自动转换你的文本消息。

从前面的内容中,你可以看出这个事件类型的原始力量,因为它可以在多个服务之间传递信息。例如,Stackdriver 支持 Cloud Pub/Sub 接口(Stackdriver 接收端)。了解这一点意味着可以使用 Cloud Pub/Sub 来在服务之间调用,例如,在 Stackdriver 中发布信息,并在 BigQuery 或 Cloud Storage 等服务中消费这些数据。

现在我们对 Cloud Pub/Sub 的功能和多样性有了更多了解,我们可以将注意力转向 Cloud Storage。

触发 Cloud Storage

如前几章所述,Cloud Storage 是一个具有生命周期和事件通知的对象数据存储。通常被称为桶(bucket),Cloud Storage 为你的工具库添加了一个高功能的通知机制。能够将中间存储集成到应用程序中,开辟了更多可能性,并且通过支持的关联事件类型,提供了将阶段门控应用于任何处理过程的手段。这种解决方案的灵活性意味着它是一个值得熟悉的服务,因为这种存储可以适用于多种场景。

存储的典型用例是临时存储(即应用程序可能需要存储一个中间文件,如音频或文本输出),或者作为一种廉价的存储形式,用于像静态网站这样的东西。在本章稍后,我们将展示一个示例,构建一个简单的网站,基于定义的模板来显示信息。

Cloud Storage 可以以多种不同的方式使用;然而,在这里,我们将主要集中在将其用作存储。事件类型关注的是通知诸如finalizedeletearchivemetadataUpdate等操作。需要注意的是,存储的通知机制利用 Cloud Pub/Sub 通知,以确保可扩展和灵活的消息传递。

在本节中,我们将涵盖 Cloud Storage 的一些最常见的用例。除此之外,事件类型还提到,你仍然可以保留所有现有的好处,例如生命周期访问、API 访问、支持不同存储类别以及安全/持久存储。

Google 提供了丰富的 API,学习如何访问这些 API 为您提供了访问不同服务的机会。由于 Cloud Storage API 的丰富性,支持许多事件类型。最常见的事件类型可能是 Cloud Storage 中对象的创建:

/**
* Triggered from a change to a Cloud Storage bucket.
*
* @param {!Object} event Data payload.
* @param {!Object} context Metadata for the event.
*/
exports.helloGCS = (event, context) => {
  const gcsEvent = event;
  console.log(`Processing file: ${gcsEvent.name}`);
  console.log(`Event Type: ${context.eventType}`);
};

在前面的例子中,当存储桶对象生成 对象创建 事件时,会触发通知。在设置函数时,一个关键要求是指定一个存储桶,函数将在其上响应操作。

值得注意的是,支持多个参数,使应用程序能够获取调用对象状态的进一步信息。如果事件传递了附加的元数据,这些高级事件元数据(如调用信息类型)也可以访问。此外,后台函数可以利用其他属性,如包含要处理的消息的数据。

从函数发出的通知在每种通知类型中都有类似的模式。您应该熟悉这种事件结构,因为无论使用何种服务,函数的使用模式都是非常一致的。

Google Cloud Storage 事件所引用的具体功能被捕获在事件参数中,这些参数会在服务运行时传递给 Cloud Function。前面列出的蓝图代码将记录已处理的事件通知以及存储在存储桶中的文件名。除此之外,更为关键的服务包括 Firebase Authentication 等额外触发器。然而,这些函数的模式在这里有所展示,随着其他触发器的可用,您应该能够平滑过渡并使用它们:

  • Google Cloud Firestore

  • Google Analytics for Firebase

  • Firebase Authentication

  • Firebase Realtime Database

  • Firebase Remote Config

到这时,您应该开始思考如何将这些触发器类型纳入到您的应用程序中。在使用函数时,重要的是要记住,服务应该是短暂的,并围绕轻量级组件构建。显然,有很多创新且实用的案例和示例将展示如何在实际应用中使用这些代码。Cloud Functions 的文档极其详细,能够回答您在实现过程中可能遇到的许多问题。

本章稍后,我们将查看一些使用这些通知类型的案例。首先,我们将看看如何利用 Google API 的丰富软件库。

探索 Cloud Functions 和 Google APIs

在这个例子中,我们将构建一个使用 Cloud Pub/Sub 提供弹性文档访问的应用程序。之前,我们介绍了我们的新朋友,Google Cloud Pub。现在我们将看到如何将此功能作为一个简单解决方案的一部分来利用。

我们的应用程序将创建一个时效性链接指向一个文本文件,只有经过身份验证的源才能访问。这个功能实际上是一个常见的日常用例,用于通过互联网安全地传输数据。

通用架构

要创建一个签名 URL,我们需要一个已经上传到存储桶中的现有文件。为了这个示例,我们将采取以下方法来开发所需的功能:

从前面的图示来看,我们将创建两个服务来创建一个签名 URL:

  1. 前端服务:一个基于 HTTP 端点的简单服务,并将公开可用。

  2. 后端服务:第二个服务,用于执行创建签名 URL 的后台功能。

接下来,让我们更详细地了解每个服务。

前端服务

我们的主要导出函数名为gcpSecureURL,它接受请求和响应参数,表明这是一个 HTTP 函数。这个签名在所有 Cloud Functions 中是一致的,提供了一种通用的方式来传递和接收应用程序中的信息。

从之前展示的架构图中,应该能明显看出我们的前端服务使用了 Cloud Pub/Sub。如本章前面所述,我们可以使用 Pub/Sub 向应用程序提供信息。在本例中,我们使用初始请求信息并将其添加到 Cloud Pub/Sub 的消息队列中。

首先,我们需要再次初始化我们的环境:

  1. 创建一个名为ch05的新目录,并将其设为当前目录。

  2. 创建一个名为frontend-service的新子目录。

  3. 创建一个名为backend-service的第二个新子目录。

  4. 到此时,您将拥有以下目录结构:

.
└── ch05
├── backend-service
└── frontend-service
  1. frontend-service设置为当前目录。

  2. 初始化该目录的npm包,即npm init --yes

  3. 然后,添加pubsub包,即npm install @google-cloud/pubsub

为了处理前端呈现的信息,我们需要创建一个新的应用程序:

  1. 创建一个名为index.js的新文件。

  2. 将以下代码添加到index.js

const {PubSub} = require('@google-cloud/pubsub');
const pubsub = new PubSub();

async function gcpCreatePayload(message) {
 const payload = Buffer.from(JSON.stringify(message));
 console.log ('Information passed: ' + message);
 await pubsub.topic('start-process').publish(payload);
}

exports.gcpSecureURL = async(req, res)=> {
 const message = req.query.message || req.body.message || 'google-cloud.png';
 await gcpCreatePayload(message);
 res.status(200).send('Creating a secure URL for:' + message);
}

代码的入口点是gcpSecureURL函数。以下是此应用程序活动的概述:

  1. 在这里,我们使用一个示例文件名(例如,google-cloud.png)调用gcpCreatePayload函数。

  2. gcpCreatePayload函数执行一个单一任务,用于创建一个新主题并将文件名发布到该主题。

  3. 执行此操作后,应用程序将返回一个200 HTTP 响应代码,并输出一条消息,指示已为文件名创建了一个安全 URL。

要部署该函数,我们按照 HTTP 端点的常规步骤操作,即以下步骤:

gcloud functions deploy gcpSecureURL — trigger-http --runtime nodejs8

如前所述,我们将使用 Cloud Pub/Sub,因此我们需要创建一个主题以便进行通信:

gcloud pubsub topics create start-process

恭喜!前端应用程序已成功部署,并准备将文件名传递给 Cloud Pub/Sub。

后端服务

现在,我们已经创建了前端服务,接下来该做什么?到目前为止,我们基本上是在创建一个待处理文件的队列。在这一部分,我们将开始处理已添加到消息队列中的文件名。

再次,我们需要重新初始化我们的环境——这一次,集中精力在backend-service子目录:

  1. 移动到之前创建的backend-service子目录。

  2. 为此目录初始化npm包,即执行npm init --yes

  3. 添加pubsub包,即执行npm install @google-cloud/pubsub

  4. 然后,添加云存储包,即执行npm install @google-cloud/storage

为了处理后端展示的信息,我们需要创建一个新的应用程序:

  1. 创建一个新的文件,命名为index.js

  2. 将以下代码添加到index.js中:

exports.gcpCreateSignedURL = (event, context)=> {
  // Get the file to be processed
  const payload = JSON.parse(Buffer.from(event.data, 'base64').toString());

  // Debug message
  console.log ('Creating a Signed URL: ' + payload);
}

在前面的代码中,我们将从消息队列中获取信息,并将其从base64格式转换。展示的信息应代表传递的示例文件名。通过控制台日志消息,我们可以确认信息已正确访问。部署此代码的初始版本,以便验证我们的假设:

gcloud functions deploy gcpCreateSignedURL --trigger-topic start-process --runtime nodejs8

到目前为止,我们已经在 Google Cloud 上部署了两个云函数:

名称 功能类型
gcpSecureURL HTTP 端点函数
gcpCreateSignedURL 背景函数

可以通过 Cloud Console 使用trigger命令来测试这些函数。进入 Google Cloud 的 Cloud Functions 选项,并选择需要的命令函数:

  1. 从菜单中选择触发器,并选择与gcpSecureURL相关的 URL。

  2. 函数将显示消息Creating a secure URL for: google-cloud.png

  3. 现在选择第二个gcpCreateSignedURL函数。

  4. 从调用列表中可以看到,这个函数已经被调用。

  5. 然后,选择查看日志选项,以查看相关的日志消息。

  6. 在日志中观察到消息gcpCreateSignedURL: google-cloud.png

恭喜!gcpCreateSignedURL(后端)服务已成功部署,并开始接收来自gcpSecureURL(前端)服务的消息。

在开发过程中,我们毫不掩饰地使用未经身份验证的函数调用,因为这使开发过程更加直接。在生产环境中,这种方法是不可接受的。由于前端和后端都有组件,一种方法是修改权限,使得只有gcpSecureURL能够调用gcpCreateSignedURL服务。在本章的后面部分,我们将得出不同的结论,来解决这个特定问题。

我们的应用程序进展得相当迅速。现在,我们需要将签名 URL 处理功能添加到现有的gcpCreateSignedURL后端代码中。

为了处理后端提供的信息,我们需要创建一个新应用:

  1. 编辑 index.js 文件。

  2. 将以下代码添加到现有 index.js 文件的顶部:

async function gcpGenerateSignedURL() {
  // Get a signed URL for the file
  storage
    .bucket(bucketName)
    .file(filename)
    .getSignedUrl(options)
    .then(results => {
      const url = results[0];

      console.log('The signed url for ${filename} is ${url}.');
    })
    .catch(err => {
      console.error('ERROR:', err);
    });
}

在前面的代码中,为了简洁,我们对存储桶和文件名做了一些假设。如你所见,函数执行的任务是创建一个签名 URL。再次强调,我们使用 console.log 函数进行调试。

此外,我们还需要向代码中添加一些定义。

  1. 将以下代码添加到现有 index.js 文件的顶部:
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();

const bucketName = 'roselabs-cloud-functions';
const filename = 'google-cloud.png';

// These options will allow temporary read access to the file
const options = {
      action: 'read',
      // MM-DD-CCYY
      expires: '11-23-2019',
};

async function gcpGenerateSignedURL() {
…
}

在代码片段中,我们需要确保项目中存在变量名 bucketNamefilename,在运行应用程序之前检查这一点。此外,确保过期日期设置为未来的日期,并注意格式要求。我们会在后续版本中修复这个问题;不过现在,只需要记住这些属性对你的项目是有效的。

最后,我们想在子目录 backend-service 中引用我们的新 SignedURL 函数:

  1. 编辑 index.js 文件并修改入口函数:
…
exports.gcpCreateSignedURL= (event, context)=> {
  const payload = JSON.parse(Buffer.from(event.data, 'base64').toString());
  console.log ('gcpCreateSignedURL: ' + payload);
  gcpGenerateCreateSignedURL();
}
  1. 基于之前的改进,我们现在准备部署更新版本的 gcpCreateSignedURL(即 backend-service)。
gcloud functions deploy gcpCreateSignedURL --trigger-topic start-process --runtime nodejs8

为了测试新部署的 backend-service,我们重复之前的过程。前往 Google Cloud 中的 Cloud Functions 选项,并通过按下 Cloud Console 中的关联 URL 来调用 frontend-service。然后,观察 backend-service 的日志,查看信息是如何处理并记录的。

恭喜!日志中包含了指向所展示文件签名 URL 的引用。我们在这一部分涵盖了很多重要的上下文。继续到下一部分,使用云存储进行示例操作。

探索 Google Cloud Storage 事件

在这个例子中,我们希望扩展对后台函数的理解。特别是,我们将与 Cloud Storage 集成,以实现自动化对象生命周期管理。

利用现有的功能(例如,包和库)是构建应用程序的绝佳方式。在这个例子中,我们将利用默认加密的 Google Cloud Storage 来开发一个安全的数据解决方案。

一般架构

与我们之前的 Cloud Pub/Sub 示例的主要区别在于我们将通知附加到 Cloud Storage。在这个示例中,我们希望存储桶发起请求来提供 SignedURL 函数,而不是手动调用我们的函数。请记住,在我们应用程序的第一个版本中,存储桶和 frontend-service 之间没有事件通知——我们来修复这一点:

从图表中,我们将引入一个额外的服务来响应存储事件通知(即,创建/完成):

  • 存储:文件已上传到存储桶。

  • 流处理:生成通知以及包含文件名的有效负载。

  • 签名 URL 前端服务:此服务保持对主题的订阅,并在接收到新有效负载时调用其函数。

  • 签名 URL 后台服务:执行创建签名 URL 的后台功能。

我们需要对现有代码进行两处修改,以启用此功能。首先,我们需要修改前端服务:

  1. 前端服务:修改代码。

  2. 存储服务:设置新的通知。

前端服务

如你所猜测的,Cloud Storage 触发器的签名与 HTTP 端点不同。因此,frontend-service子目录中的代码需要做一个小的修改,以保持兼容性。我们需要修改应用程序代码,以便正确处理信息:

  1. frontend-service子目录中,编辑index.js文件。

  2. 移除现有的gcpSecureURL函数,因为它与 HTTP 端点相关。

  3. 添加以下代码,以便处理 Cloud Storage 触发器:

exports.gcpSecureURL = (data, context)=> {
  // Get the file to be processed
  const message = data;

  // Create a pubsub message
  gcpCreatePayload(message.name);
};

注意,gcpSecureURL的签名现在使用了数据和上下文,因为我们现在引用的是 Cloud Storage 中的信息。与之前使用 HTTP 触发器的请求和响应相比,请进行对比。

恭喜!gcpSecureURL 前端服务现在能够接受来自 Cloud Storage 的通知。让我们继续为我们项目中使用的存储桶建立合适的触发器。

存储服务

为了启用存储服务,与前一部分不同,我们需要让 Google Cloud Storage 知道我们想接收通知。由于与存储相关的 API 功能非常丰富,我们还需要使用另一个命令gsutil

从命令行,我们需要告诉存储桶我们希望监控哪些通知事件。此外,我们还希望它在发生某些事件时通知我们。第四章,开发云函数,包含了 Cloud Storage 支持的触发器。具体来说,和我们任务相关的是google.storage.object.finalize(即finalize/create活动),它包括基于存储桶内对象创建的事件:

  1. 删除现有的gcpSecureURL云函数。

  2. 使用基于存储桶资源的触发器部署frontend函数:

gcloud functions deploy gcpSecureURL \
 --runtime nodejs10 \
 --trigger-resource gs://roselabs-signed-upload \
 --trigger-event google.storage.object.finalize

在上面的代码中,我的存储桶被定义为roselabs-signed-upload,这是我应用程序独有的。在你的项目中,存储桶的名称应根据你的 Google Cloud 项目设置进行命名。

  1. 向存储桶上传一个文件。这可以是你手头的任何文件。

  2. gcpSecureURL将通过 Cloud Pub/Sub 通知被调用。

恭喜你,将通知集成到应用程序中是一个巨大的时间节省。在下一部分中,我们将增强功能,学习如何将其与 Google Cloud 服务集成。

构建增强版签名 URL 服务

在应用程序的最终修订中,我们将修复一些明显的问题,特别是 Cloud Function 的问题。需要解决的主要问题是存储桶引用、文件名和到期日期的硬编码。一个可选方案是为应用程序提供一个漂亮的图形前端界面。然而——剧透警告——我们在这里不会采用这种方法。

为了完成应用程序,我们需要纠正三件事,以使 Cloud Function 更加有用:

  1. 文件名

  2. 存储桶引用

  3. 到期日期

小的改变可能带来实质性的影响,这三项修改将大大改善应用程序。为了进行这些修改,我们需要同时修改两个服务,并从一个服务传递额外的变量到另一个服务。记住,我们之前已经实现了 Cloud Pub/Sub 来在服务之间传递信息。现在我们需要扩展这一方法。但这会使任务更难吗?通过逐步增强组件并讨论其影响,我们将在开始前端服务的增强之旅时解答这个问题。

前端服务

就前端服务而言,应用程序当前将文件名作为负载发送。然而,我们在这里漏掉了一个技巧,因为 Cloud Storage 数据对象实际上包含一些非常有用的信息。

Cloud Storage 上下文对象

以下表格包含与上下文对象相关的信息。

Object Field
Context eventId
Context eventType

Cloud Storage 数据对象

以下表格列出了与数据对象相关的信息。如你所见,这里有一些可以在应用程序中使用的有用信息。

Object Field
data bucket
data name
data metageneration
data timeCreation
data updated

根据前面的信息,我们需要的很多数据都可以在数据对象中找到。在这种情况下,我们可以将该对象传递到消息队列中,供后端进一步处理:

  1. 进入 frontend-service 子目录。

  2. 修改 entrypoint 函数,其修改如下:

exports.gcpSecureURL = async (data, context)=> {
  // Get the file to be processed
  const message = data;

  // Create a pubsub message based on filename, bucketname
  await gcpCreatePayload(message);
}
  1. 然后,按如下方式修改 gcpCreatePayload 函数:
async function gcpCreatePayload(message) {
  // Process a Pub/Sub message - amend to a JSON string
  const payload = Buffer.from(JSON.stringify(message));

  console.log ('Information passed: ' + payload);

  // Pass the Topic and the payload
  await pubsub.topic('start-process').publish(payload);
}

恭喜!gcpSecureURL 函数现在将数据对象转发到 Cloud Pub/Sub。我们不再需要编写额外可能复杂的代码,而是利用 Cloud Pub/Sub 消息传递在服务之间传递数据。

后端服务

后端服务将不会向 entrypoint 函数提供数据对象。我们需要从数据对象中提取文件名和存储桶名称,而不是直接访问文件名:

  1. 进入 backend-service 子目录。

  2. 修改 gcpCreateSignedURL 函数,修改内容如下:

exports.gcpCreateSignedURL= (event, context)=> {
  // Get the file to be processed
  const payload = JSON.parse(Buffer.from(event.data, 'base64').toString());

  // Debug message
  console.log ('gcpCreateSignedURL: ' + payload.name + ' ' + payload.bucket);

  // Call the function
  gcpGenerateSignedURL(payload.name, payload.bucket);
}
  1. 修改 signedURL 函数:
async function gcpGenerateSignedURL(filename, bucketName) {
  // Get a signed URL for the file
  storage
    .bucket(bucketName)
    .file(filename)
    .getSignedUrl(options)
    .then(results => {
      const url = results[0];
      console.log('The signed url for ${filename} is ${url}.');
      // gcpMessageQueue(url);
    })
    .catch(err => {
      console.error('ERROR:', err);
    });
}

当我们处于backend-service子目录时,我们还可以修复硬编码的过期日期。解决过期日期问题的一种简单方法是为签名 URL 添加标准时长。添加时长意味着我们可以自动提供一个未来的日期,届时提供的 URL 将自动过期:

  1. 转到backend-service子目录。

  2. gcpGenerateSignedURL函数上方添加一个过期日期函数:

function gcpExpirationDate(duration) {
  const ExpirationDate = new Date();

  ExpirationDate.setDate(ExpirationDate.getDate() + duration);
  futureDate = ((ExpirationDate.getMonth()+1) + '-' + ExpirationDate.getDate() + '-' + ExpirationDate.getFullYear());

  console.log ('Expiration date: ${futureDate}');

  return (futureDate);
}

async function gcpGenerateSignedURL(filename, bucketName) {
…
}
  1. 修改options对象,以便调用gcpExpirationDate函数:
const MAX_DURATION_DAYS = 7

const options = {
      action: 'read',
      // MM-DD-CCYY
      //expires: '11-23-2019',
      expires: gcpExpirationDate(MAX_DURATION_DAYS),
};

与其添加复杂的代码,我们传递 Cloud Storage 提供的当前信息,这使得我们的服务能够利用完整的数据集。此外,我们现在为签名 URL 设置了一个固定的时长,因此整个过程从头到尾完全自动化。

继续删除项目中的现有 Cloud Functions,并清空存储桶中的内容。我们将重新部署函数,这是最后一次部署。然而,这次当被问及是否允许未经身份验证的调用时,请选择

  1. 部署更新后的前端服务:
gcloud functions deploy gcpSecureURL \
 --runtime nodejs8 \
 --trigger-resource gs://roselabs-signed-upload \
 --trigger-event google.storage.object.finalize
  1. 部署后端服务:
gcloud functions deploy gcpCreateSignedURL --trigger-topic start-process --runtime nodejs8
  1. 一旦 Cloud Functions 部署完成,上传一个新文件到存储桶。

一旦用户将文档上传到存储桶,自动化流程就会接管。我们的功能仍然有效,因为它们使用与项目关联的服务帐户。Cloud Functions 不再允许未经身份验证的调用访问。

Cloud Storage 的数据对象提供了我们服务所需的所有信息,以实现完全自给自足(即文件名和存储桶名称)。现在,应用程序通过服务帐户无缝地在服务之间传递信息,我们不再需要过多担心以下问题:

  • Cloud Functions 的安全性,由服务帐户管理它们

  • 用户验证,因为存储桶中的信息在文件上传时会自动提供

  • 扩展我们的功能,因为数据使用了 Cloud Storage 和 Cloud Pub/Sub

恭喜!现在backend-service正在使用从 Cloud Pub/Sub 消费的数据对象。在我们结束这一话题之前,请花一点时间思考一下我们的解决方案变得多么灵活。我们从一个简单的需求开始,通过进行一些小的增量更改,现在我们已经拥有了一个完全自动化的服务。由于同时包含了 Cloud Pub/Sub 和 Cloud Storage,我们还拥有一个可扩展的解决方案,并且内建了弹性。架构保持了各功能之间的松散耦合,因此我们可以继续迭代设计,而不必担心破坏与组件之间的链接,这得益于 Cloud Pub/Sub 消息的引入。

总结

到这一点,你应该对 Cloud Functions 提供的一般架构和组件有一个合理的理解。虽然 Cloud Functions 的典型用例是使用 HTTP 端点,但拥有后台功能(例如 Cloud Pub/Sub 和 Cloud Storage)来集成不同的服务,通过标准化接口进行集成,也是非常有用的。我们使用 HTTP 端点和后台功能,成功地从本地开发环境中原型化了一个简单的服务应用程序来创建 signedURL 函数。在前几章所学的基础上,我们已经能够完成大部分工作。

云函数开发应用程序的过程展示了一个简单的解决方案如何能够快速构建和扩展。Cloud Pub/Sub 需要定义一个消息队列,提供将不同服务集成的能力。能够解耦数据的后台处理使得解决方案能够松散耦合,更容易与更广泛的技术解决方案集成。在许多情况下,Cloud Pub/Sub 作为 Google Cloud 的粘合剂,提供多种服务之间的数据交换。Cloud Storage 提供了一种简单而有效的方式,允许用户上传数据,而无需暴露门户或创建复杂的生命周期管理代码。

在下一章中,我们将创建一些通用示例,以便在我们迄今为止学到的技术基础上继续构建。我们的重点将继续放在 Cloud Functions 和构建组件上,以激发我们开发更完整、更具挑战性的应用程序的兴趣。本章内容的重点将是提供一系列更全面的示例,并涵盖一些将 Cloud Functions 纳入你的项目组合所必需的元素。

问题

  1. Cloud Pub/Sub 支持哪些订阅类型?

  2. 列出三种 Cloud Pub/Sub 的消息设计模式?

  3. 与 HTTP 相关的动词有哪些?

  4. 如果我希望用户数据能在 URL 中访问,我应该使用 GET 还是 POST?

  5. 哪个 Cloud Storage 属性维护关于数据内容的信息?

  6. 如果我的代码响应一个 5xx 错误代码,我应该在何处找到这个错误?

  7. Google Cloud 是否支持 OAuth v2?

  8. 如果一个 Cloud Pub/Sub 消息在截止时间前没有被确认,消息会丢失吗?

进一步阅读

第六章:云函数实验

在上一章中,我们通过构建一个访问签名 URL 的应用程序,掌握了一些有用的技能。最常见的例子之一就是构建一个网站。因此,我们将通过创建一个基于静态网站的示例数据库,扩展我们的技能库,内容将围绕漫威电影展开。到本章结束时,你将理解如何通过丰富的代码示例增强之前介绍的各种技术。

在我们的示例中,我们将探讨如何将数据整合进来,并首次审视服务账户的安全性。除此之外,我们将查看静态网站的主要组件以及如何可能提升这些组件的功能。

本章将涵盖以下主题:

  • 构建静态网站

  • 服务账户安全

技术要求

为了完成本章的练习,你需要一个 Google Cloud 项目或 Qwiklabs 账户。

你可以在本书的 GitHub 仓库中的ch06子目录找到本章的代码文件,网址为github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch06

在阅读书中的代码片段时,你会注意到,在少数情况下,代码/输出的某些行已被删除,并用省略号(...)代替。省略号的使用是为了仅展示相关的代码/输出。完整的代码可以通过前述链接在 GitHub 上获取。

构建静态网站

在下面的示例中,我们将构建一个应用程序,在这个应用程序中,我们熟悉的函数基线代码将调用一个外部数据源(基于 JSON),并输出一个通过 HTML 模板渲染的视图。

环境配置

我们之前在其他 Node.js 应用程序中看过这种布局(例如,第三章,介绍轻量级函数 和 第四章,开发云函数),所以在本章中,重点将放在构建我们网站所需的代码上。

首先,按照以下步骤操作:

  1. 创建一个全新的目录来托管我们的代码。

  2. 在新目录中,通过在命令行中执行npm init --yes来初始化环境,以初始化我们的新开发环境。

  3. 根据下表完成生成的package.json

字段 响应
包名称: marvel-website
版本: 1.0.0
描述: 这是一个使用云函数构建的示例网站
入口点: index.js
测试命令: 空白
Git 仓库: 空白
关键词: 空白
作者: 输入你的名字
许可证: ISC

如在第四章 开发云函数和第五章 探索函数即服务中所述,为了本地运行,我们可以使用functions-framework包。此外,我们还需要安装pug包,用于渲染 HTML 的视图模板。

  1. 从命令行安装必要的包,使用以下命令:
npm install @google-cloud/functions-framework
npm install pug
  1. 接下来,创建几个新的子目录,用于存放与该功能相关的viewsdata
mkdir data && mkdir views
  1. 到此为止,您的目录结构应类似于以下内容:也就是说,两个文件(package.jsonpackage-lock.json)和三个子目录(dataviewsnode_modules):
.
├── data
├── node_modules
├── package.json
├── package-lock.json
└── views

请记住,为了在您的应用程序中使用 Functions Framework,您需要修改package.json文件,以包含一个start属性。

  1. 编辑package.json文件,并按照这里所述的方式添加对 Functions Framework 的引用:
  0 { 
  1   "name": "marvel-website", 
  2   "version": "1.0.0", 
  3   "description": "This is an example website built on Cloud Functions with Nodejs", 
  4   "main": "index.js", 
  5   "scripts": { 
  6     "start":"functions-framework --target=filmAPI", 
  7     "test": "echo \"Error: no test specified\" && exit 1" 
  8   }, 
  9   "author": "Rich Rose", 
 10   "license": "ISC", 
 11   "dependencies": { 
 12     "@google-cloud/functions-framework": "¹.2.1", 
 13     "pug": "².0.4" 
 14   } 
 15 }

第 6 行中突出显示的信息显示了必要的启动命令。请注意,目标入口点名为filmAPI,这意味着我们应用程序中导出的函数也需要匹配这个签名。

很好!我们现在已经创建了应用程序的基本结构。接下来,我们将注意力转向提供信息的源头,这些信息将在应用程序中展示。

创建数据源

现在,我们将注意力转向数据目录,下一步是创建一个JavaScript 对象表示法JSON)文件来存储我们的数据。使用 JSON 是一种快速而简单的方式来创建外部数据源,如下所示:

  1. data子目录中创建一个名为films.json的新文件。这个文件将存储我们的电影信息。

  2. 对于第一部电影,添加以下表格中的内容:

为了解释所使用的架构,让我们先看看文本的内容。主要结构是一个数组,我们将在其中创建多个占位符,用于表示电影的对象。电影对象将暴露几个字段:

字段 类型 注释
title 字符串 电影标题
director 字符串 电影导演的名字
release 字符串 电影的发布日期
description 字符串 电影情节的总体概述
bgImage 字符串 电影海报的 URL

直接查看 JSON 文件,我们可以看到数据在文件中的表示方式:

请注意代码块末尾的逗号;这表示我们将要在文件中添加进一步的内容。如果我们不添加其他内容,则在电影对象后面不会加逗号(请参见The Avengers对象的比较)。

{
  "movies": [
  {
    "title": "Iron Man",
    "director": "Jon Favreau",
    "release": "2008",
    "description": "Iron Man",
    "bgImage": "https://upload.wikimedia.org/wikipedia/en/7/70/Ironmanposter.JPG"
  }, 
  1. 添加像The Incredible Hulk这样的额外内容可以通过在数组记录末尾追加内容来实现:
  {
    "title": "The Incredible Hulk",
    "director": "Louis Leterrier",
    "release": "2008",
    "description": "The Incredible Hulk",
    "bgImage": "https://upload.wikimedia.org/wikipedia/en/8/88/The_Incredible_Hulk_poster.jpg"
  },
  1. Iron Man 2添加一条新记录:
  {
    "title": "Iron Man 2",
    "director": "Jon Favreau",
    "release": "2010",
    "description": "Iron Man 2",
    "bgImage": "https://upload.wikimedia.org/wikipedia/en/e/ed/Iron_Man_2_poster.jpg"
  },
  1. 基于Thor添加一条新记录:
  {
    "title": "Thor",
    "director": "Kenneth Branagh",
    "release": "2011",
    "description": "Thor",
    "bgImage": "https://upload.wikimedia.org/wikipedia/en/f/fc/Thor_poster.jpg"
  },
  1. Captain America添加一条新记录:
  {
    "title": "Captain America: The First Avenger",
    "director": "Joe Johnston",
    "release": "2011",
    "description": "Captain America: The First Avenger",
    "bgImage": "https://upload.wikimedia.org/wikipedia/en/3/37/Captain_America_The_First_Avenger_poster.jpg"
  },
  1. 最后,为The Avengers添加一条新记录:
 {
    "title": "The Avengers",
    "director": "Joss Whedon",
    "release": "2012",
    "description": "Marvel's The Avengers",
    "bgImage": "https://upload.wikimedia.org/wikipedia/en/f/f9/TheAvengers2012Poster.jpg"
  }
  ]
}

基于前面的代码,我们已经创建了六个新的记录,用于在数组中保存数据。目的是让这些数据作为我们即将创建的云函数的输入。在实际应用中,更可能使用数据库来满足这种访问方式。然而,在这个示例中,简洁是我们的朋友,JSON 也是!

现在我们已经创建了数据源,接下来需要实现屏幕上的展示,以呈现要显示的信息。

设计前端

使用 HTML 制作前端应该不会带来太大的挑战,因为有很多优秀的示例可供参考。在这一部分,我们将采用我们的模式并将其显示在屏幕上。所使用的模板将在views子目录中创建,并以一个名为index.pug的新文件形式呈现,文件内容如下:

  • 头部定义

  • 卡片定义

  • Release 定义

  • 描述定义

  • Body 定义

根据前面的要点,让我们来看一下每个部分的内容:

  1. 头部定义使用标准的 HTML 来结合样式内容。在示例中,我们创建了一个新的头部样式,用于更改文本的字体和对齐方式:
#message
html
  head
    style.
      .header {
         text-align: center;
         font-family: roboto;
      }
  1. card定义创建了一个新的样式,视觉上看起来像一个屏幕上的卡片:
      .card {
         box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
         max-width: 600px;
         margin: auto;
         text-align: center;
         font-family: roboto;
      }
  1. release定义创建了颜色和字体的增强效果:
      .release {
         color: grey;
         font-size: 22px;
      }
  1. description定义通过参考颜色、字体大小和内边距等内容,对与该样式相关的任何元素进行增强:
     .description {
         color: black;
         font-size: 18px;
         padding: 8px;
      }
  1. body定义提供了通过云函数导入数据的主要布局考虑:
 body

    div.header
      H1 Functions Framework Example
      H3 Google Cloud - Cloud Functions
    div.card
      img(src=items.bgImage style="width:100%")
      h1 #{items.title}
      div.release
        h3 Released: #{items.release} <br>
        h3 Director: #{items.director} <br>
      div.description
        p #{items.description}

让我们分解一下:

名称 类型 备注
H1 主要标题 页面标题
H3 子标题 页面的子标题
img 图片链接 图片的 URL 链接
p 段落 描述文本

在创建视图时,我们使用从云函数传递过来的信息来填充屏幕上的视图。具体来说,我们从 JSON 数据文件中的模式信息中提取标题、发布日期、导演和描述等内容。

在这个示例的这个阶段,基于我们在示例中创建的文件,我们应该已经有了以下的目录结构:

├── data
│ └── films.json
├── node_modules
├── package.json
├── package-lock.json
└── views
    └── index.pug

我们还创建了一个基于 JSON 文件的数据源,其中包含关于电影的信息。添加新电影、移除电影和更改电影相关信息的方法应该很直观,数据文件为管理外部数据提供了一个方便的方式。而且,视图是与主功能分离的;因此,作为额外的好处,展示层独立运行,方便进行修改。最后,我们需要创建服务来实现数据和视图之间的集成,并生成适当的 HTTP 响应。

分析云函数

我们定义的云函数没有太多的代码与之相关。我们的云函数中包含以下元素:

  • 变量定义

  • 私有函数

  • 公共函数

为了解释我们在这里概述的各种元素,让我们花一点时间更详细地探讨每个组件。

变量定义如下:

  • 第 1 行:函数以声明包含我们的临时数据存储开始,其中存储了电影信息。同样,这是一种将数据整合到应用程序中的简单方式,无需额外的基础设施支持。

  • 第 2 行:使用pug包依赖并声明在函数中使用:

  0 // Define dependencies 
  1 var data = require('./data/films.json'); 
  2 var pug = require('pug'); 

私有函数的工作原理如下:

  • 第 6 行:声明要根据index.pug文件中定义的外部视图进行渲染的视图

  • 第 11 行:根据 HTTP 状态响应码 200 以及基于用户在查看初始页面时选择的项目创建响应对象:

  3 
  4 // Function: marvelFilm Detail 
  5 // Description: Show the information for the film selected 
  6 function filmDetail(req, res, movieRef) { 
  7   // Define the view to be displayed 
  8   const pugInputFile = pug.compileFile('views/index.pug'); 
  9 
 10   // Create the HTML view 
 11   res.status(200).send(pugInputFile({ 
 12     // Pass data object [movies] to Pug 
 13     items : data.movies[movieRef] 
 14   })); 
 15 } 

公共函数的工作原理如下:

  • 第 31 行:调用filmDetail函数时,传递请求、响应和数据对象:
 16 
 17 // Entrypoint: marvelFilmAPI 
 18 // Description: This is the Cloud Function endpoint 
 19 exports.filmAPI= (req, res) => { 
 20   // Define the default view to be displayed 
 21   let filmNum = req.query.film || '00'; 
 22 
 23   // Translate string to int 
 24   var movieRef = parseInt(filmNum, 10); 
 25 
 26   // Simple validation 
 27   if (movieRef > 5 || movieRef < 0) 
 28     movieRef = 0; 
 29 
 30   // Display the relevant film 
 31   filmDetail(req, res, movieRef); 
 32 }; 

运行上述应用程序(即从命令行运行npm start)时,将显示 JSON 电影列表数组的详细信息。

请注意,查询字符串 URL 可以用于访问第一部电影之外的信息。像这样的 HTTP 查询机制在向子组件传递附加参数时非常有用。在此示例中,我们使用这些参数来选择显示不同的页面,而无需修改现有的函数代码。

要从可用电影数组中选择另一部电影,请使用 URL 查询设置来访问电影数据库中保存的任何电影,如下表所示:

电影 URL
1. 铁人 http://localhost:8080
2. 无敌浩克 http://localhost:8080/?film=1
3. 钢铁侠 2 http://localhost:8080/?film=2
4. 雷神 http://localhost:8080/?film=3
5. 美国队长:复仇者先锋 http://localhost:8080/?film=4
6. 复仇者联盟 http://localhost:8080/?film=5

如果你从云终端运行此应用,请注意 URL 会与上面显示的不同。但你仍然可以附加查询设置,例如https://mydomain-dot-devshell.appspot.com/?authuser=0&environment_id=default&film=2来显示第二部电影。

当向应用程序提交不同的查询对象时,您将看到不同的页面。

这个例子展示了如何通过查询参数扩展 GET HTTP 方法。正是这种灵活性使得 HTTP 成为如此广泛采用的方法,并展现了该协议的优势。我们的示例展示了如何使用云函数开发一个简单的 Web 应用程序。这个例子当然可以扩展到其他更复杂的用例。我将把根据前几章所学的知识构建下一个大项目的任务留给你发挥想象力。

现在我们将注意力转向 Cloud Functions 安全这一关键话题。在接下来的部分,我们将讨论在使用 Cloud Functions 时可以应用的各种安全技术。

服务账户安全

最后,在本章中,我们将介绍关于身份访问管理IAM)的最小权限概念,并解释如何将其应用于 Cloud Functions。我们将探讨一些可以保障使用 Cloud Functions 的应用程序安全的方法。

本章稍后将讨论服务账户;然而,首先简要看一下如何限制已部署的 Cloud Functions 的调用者状态。Cloud Functions 使用服务账户而非用户账户来管理服务。在这方面,服务账户扮演了用户的角色,而无需实际的人类参与此过程。

关于用户账户,每个已部署的函数将分配一个服务账户来负责权限。服务账户可以手动创建,也可以自动创建;无论哪种方式,都需要定义角色和权限。当函数被部署时,通常会为服务账户应用 cloudfunctions.invoker 权限。该权限授予服务账户调用/触发 Cloud Functions 的能力。

除了此权限外,调用者还需要对该函数进行正确的身份验证。如果默认设置保持不变,则 allUser/public 接口将绑定到该服务,意味着任何人都可以调用该函数。HTTP 函数通常会使用默认的 allUsers 策略绑定进行部署,这意味着任何人都可以调用该服务。这样,任何人都可以通过暴露的端点来调用我们的函数。开放访问为我们的应用程序带来了潜在的安全漏洞,需要一个解决方案来减轻这一风险。如果您希望限制对该函数的访问,请将其限制为安全账户访问,例如 allAuthenticated 或通过其 IAM 账户指定的特定用户。

尽管 Cloud Functions 的运行时间较短,但它们仍然容易受到安全风险的威胁,例如恶意代码或服务拒绝攻击。

请注意,关于拒绝服务攻击,Cloud Functions 位于 Google 前端后面,该前端用于缓解和吸收许多 4 层及以下的攻击,例如 SYN 洪水、IP 分片洪水、端口耗尽等。

有许多不同类型的漏洞可能对基于 Web 的服务构成问题。由于该主题的复杂性,我们无法提供超出当前防护措施的更多概述。潜在的漏洞范围似乎在不断增加,例如恶意代码、拒绝服务攻击或事件资源瓶颈等。

以下部分详细介绍了一些技术,用于缓解 Cloud Functions 的安全问题。

经济限制

正如我们现在所知,serverless 提供了在 Cloud Functions 中实现可扩展性的能力。然而,集成了 Cloud Functions 的服务可能无法应对这种级别的扩展。在您的应用中,如果需要,您可以限制可用实例的最大数量。

应用连接性限制可以减轻负担,并防止服务遭遇过多无法成功处理的请求。通过设置实例的可用数量阈值,意味着在部署时可以限制该功能,如以下命令所示:

gcloud beta functions deploy … \
--max-instances 4

在这里的受限场景中,启用了排队机制,以确保所施加的限制不会妨碍信息处理到您的服务。限制实例阈值是确保服务调用安全的一个良好开端。另一种变体则关注如何获取访问资源所需的凭证。

不安全的凭证存储

凭证存储是确保各种服务安全性时日益重要的话题。重要凭证(或密钥),例如数据库用户名/密码/发布凭证或存储在代码中的密钥,需要一个解决方案,以确保这些信息只能被经过身份验证的组件访问。

许多行业解决方案将提供必要的保护,包括 Google Cloud 密钥管理服务 (KMS),它支持使用 Google 或客户提供的密钥。此外,一些通用产品如 HashiCorp Vault 也为存储信息提供了类似级别的安全保护。

因此,凭证提供了更好的保护并允许服务得到安全保障。当与多个集成服务一起工作时,如何管理工作流以达到所需的安全级别呢?

执行流操作

执行流基本上是描述您的应用程序功能工作流的方式。在您有多个功能的场景中,组件之间可能会进行信息交接,它们交换信息或提供对后端服务(如数据库)的访问。

在这种情况下,任何功能都不应被拦截或接受来自未经身份验证的外部来源的通信。这样做将降低工作流的完整性。像这样的情况会给应用程序带来实质性的安全风险(例如,信息被篡改)。

为了防止这种类型的漏洞,我们可以利用最小权限安全设置来管理服务帐户,并建立按功能的授权机制来验证调用。默认情况下,所有功能/应用程序/容器可以共享相同的服务身份,并分配角色,如项目编辑者。然而,身份管理也可以按功能进行设置,以建立最小权限。对于云功能,可以在部署时附加服务帐户来控制功能。

在下面的图示中,能够调用 Cloud Function 的服务帐户标记为Function A

服务帐户部署为我们的其他服务提供了一个公共接口。我们还将为Function B创建第二个服务帐户来管理后台;该服务帐户将没有调用者权限,并且不会公开。最后,后端服务不会向公众互联网开放。除此之外,它们还将可以从 Function B 访问。这种安排相对常见,例如在堡垒主机上,堡垒主机面对互联网,且只有主机有权限与庄园中的其他机器连接。

根据谷歌的说法,服务帐户是一种特殊帐户,它可以在没有用户参与的情况下调用 Google API。这些计算机帐户是非常有用的功能,并通过 IAM 权限进行定义。我们可以使用以下 gcloud 命令创建一个新的服务帐户,该帐户将提供对该功能的访问权限:

  1. 创建服务帐户—Function A
gcloud iam service-accounts create publicCloudFunction
  1. 将角色绑定到服务帐户,使其能够使用 invoker 角色:
gcloud functions add-iam-policy-binding SaveData \
--member='serviceAccount:publicCloudFunction@projectid.iam.gserviceaccount.com' \
--role='roles/cloudfunctions.invoker'

启动 Cloud Functions 需要 cloudfunctions.invoker 角色,我们将此权限绑定到我们的新服务帐户。通过这样做,我们的服务帐户现在可以调用 Cloud Functions。

现在我们已经创建了具有必要权限的服务帐户,我们可以通过附加帐户名称来使用新服务帐户部署功能。

  1. 在部署 Cloud Function 时应用新的服务帐户:
gcloud functions deploy …\
--service-account account-privilege@projectid.iam.gserviceaccount.com

为了完成我们的示例,我们将创建一个与第二个功能相关的第二个服务帐户。对于此服务帐户,我们将不应用 invokercloudfunctions.invoker)角色,从而限制谁可以启动该功能。

  1. 创建服务帐户—Function B
gcloud iam service-accounts create saveData

如同我们在第一个服务帐户中所做的那样,我们需要绑定一些权限。在这个例子中,我们使用的是 Cloud SQL 权限。

  1. 将角色绑定到服务帐户:
gcloud projects add-iam-policy-binding projectid \
--member='serviceAccount:saveData@projectid.gserviceaccount.com' \
--role='roles/cloudsql.client'

最后,我们可以使用最近创建的服务帐户来部署该功能。

  1. 在部署的功能上应用服务帐户:
gcloud functions deploy saveData \
--service-account saveData@projectid.gserviceaccount.com

到此为止,我们已经对应用程序进行了简单的修改,以确保函数执行流程符合我们的需求。Function A 是我们的入口函数,并为我们的应用提供前端。现在,Function B 仅通过 Function A 访问,从而确保了从 A 到 B 的执行流程的安全性。同时请注意,现在 Backend Services 仅通过 Function B 访问。根据前面步骤中指定的更改,我们现在有了以下布局:

在最后一个要讨论的领域中,我们将通过策略控制来研究安全控制。

FunctionShield

Google Cloud Functions 还支持诸如 FunctionShield 等第三方解决方案。通过这些解决方案,采用严格的安全控制,确保对已部署的函数应用策略。具体来说,这些保护措施适用于四个不同的领域:

  • 安全策略允许禁用出口互联网连接。当服务不需要外向流量时,限制出站流量是一个好习惯,因为大多数服务通常只需要启用入口/入站流量。

  • 如前所述,Cloud Functions 是轻量级的无状态函数。在这方面,应该尽可能限制本地存储需求。因此,FunctionShield 可以禁用对 /tmp 目录的读写操作,该目录通常用于中介存储需求。

  • 在没有被调用的函数要求时,应尽量减少和限制子进程的线程执行。能够执行子进程带来了一个真实的安全风险,一旦启动,没有充分的保护措施很难追踪和追溯。

  • 通过中央控制台限制对函数源代码的访问。当源代码具有一定价值或关联知识产权IP)时,这可以作为一种额外的保障措施,应用于要部署的系统源代码。

FunctionShield 解决方案是一个免费产品,可以部署到多个云提供商,因此它是一个灵活的解决方案,我们可以用它来确保在无服务器环境中应用标准策略。部署到应用程序中不需要修改函数代码。一个基于行为的专有运行时在无服务器环境中建立保护。云函数的可观察性保持清晰,并且可以通过标准机制获取:Google Cloud 上的 Stackdriver。

总结

本章已结束我们在 Google Cloud 上对 Cloud Functions 的探索。过去几章涵盖了很多关于如何开发 Cloud Functions 的内容。在第一个示例中,我们继续使用 Cloud Functions 探讨如何基于外部视图和数据模板构建一个网站。在这个示例中,我们总结了一些关于代码组织和依赖隔离的技巧。

在完成这个示例后,我们得到了一个易于维护的网站,利用 Cloud Functions 提供了一个可扩展的轻量级站点访问基础。我们学习了最小权限的原则,以及它如何应用于 Cloud Functions。同时,我们还学习了一些基本方法,能够通过与 Google Cloud 服务账号协作来确保我们的 Cloud Functions 安全。

在这个阶段,你应该已经能够熟练构建集成了 Google API 的 web 应用程序和服务。还有更多内容可以学习,不过我们在这本书中没有足够的时间或篇幅来涵盖。Cloud Build、GKE、数据库、物联网以及第三方服务都是进一步探索的潜在方向。这就结束了我们关于 Google Cloud 上 Cloud Functions 的介绍。在接下来的章节中,我们将把注意力转向 Google Cloud 上最新的无服务器迭代:Cloud Run 和 Cloud GKE。

问题

  1. Cloud Function 需要什么权限才能被调用?

  2. Google KMS 提供什么功能?

  3. allUsers 权限的实际含义是什么?

  4. allAuthenticated 权限的实际含义是什么?

  5. 限制 Cloud Function 可用实例数的参数是什么?

  6. 哪个命令允许将角色绑定到服务账号?

  7. 用于创建服务账号的命令是什么?

  8. 什么是堡垒主机,它有什么用?

进一步阅读

第三部分:Google Cloud Run

在本节中,您将了解 Cloud Run,这是 Google 推出的最新产品,用于管理无服务器工作负载。Cloud Run 可以在有或没有 Kubernetes 平台的环境中进行部署,因此初步讨论将集中在非 Kubernetes 环境的基础知识上。在这里,我们将学习关于容器的内容以及 Google Cloud 上的一些开发者支持工具(例如 Cloud Build 和 Container Registry)。接下来,我们将探讨如何将 Cloud Run for Anthos 与 Kubernetes 配合使用。最后,在这一系列章节中,我们将讨论 CI/CD 主题。

本节包括以下章节:

  • 第七章,介绍 Cloud Run

  • 第八章,使用 Cloud Run 开发

  • 第九章,使用 Cloud Run for Anthos 开发

  • 第十章,Cloud Run 实验

第七章:介绍 Cloud Run

到目前为止,在本书中我们讨论了许多与构建云端无服务器技术相关的内容。在本章中,我们将重点介绍 Google 最新推出的产品,它为你的应用提供了一个无状态的环境。与 Cloud Functions 不同,Cloud Run 明确利用容器技术为 HTTP 端点提供受限环境。而 Cloud Functions 则为无服务器工作负载提供了一种有局限性的视角,例如,运行时语言的限制。Cloud Run 则去除了许多这些限制,以便更好地满足开发者的需求。如果你仔细研究这些内容,你会发现容器和 Kubernetes 是任何云计算专业人士的必备技能。

为了开始我们的讨论,我们将概述 Cloud Run 组件架构。在此过程中,我们将讨论几个主题,以便为 Cloud Run 设置背景。本章的主要目标是介绍支持技术。你应该花时间理解这些使用案例,并了解 Cloud Run 如何利用每个技术。

在继续讲解 Cloud Run 组件架构之前,我们将先奠定一些基础,概述一些关键技术。为了开始这个讨论,我们将从微服务谈起。

简而言之,本章将涵盖以下主题:

  • 使用微服务

  • 使用容器

  • 介绍 Cloud Run

  • Cloud Run 与 Cloud Run for Anthos 的区别

技术要求

要完成本章的练习,你需要一个 Google Cloud 项目或一个 Qwiklabs 账户。

你可以在本书的 GitHub 仓库中找到本章的代码文件,位于ch07子目录下,网址是github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch07

在阅读本书中的代码片段时,你会注意到,在一些情况下,代码或输出的某些行被删除并用省略号(...)替代。使用省略号只是为了展示相关的代码或输出。完整的代码可以在之前提到的 GitHub 链接中找到。

使用微服务

关于单体架构与微服务架构的关键优势,已经有了大量的讨论。创建更小的代码包的可能性显然具有优势,因为它们通常更容易调试,更简单集成,且具有一致的消息接口。然而,这些优势本身并不足以促使我们完全迁移到微服务架构。

在下图中,我们将典型的单体软件结构与微服务架构进行对比。首先要注意的是,微服务架构提供了更多可用的组件服务。需要特别指出的一点是,将单一应用程序解构为专注于业务操作的服务交付。在接下来的几段中,我们将讨论这种方法背后的逻辑,以及在迁移到云运行等环境时它的好处(以及高度相关性):

首先,微服务应该是自主的;也就是说,它们提供一个隔离且独立的组件。提供标准化接口可以让微服务无缝地参与到更广泛的生态系统中。确保各个组件之间能够实现通信,为构建可扩展且灵活的解决方案提供了基础。

从微服务的角度来看,它们通常服务于多个已经部署在松耦合架构中的容器组件。每个微服务代表了一个应用程序被分解成一系列功能的过程。考虑到我们如何专注于为云函数(参考第三、四和五章)构建具有单一目的的轻量级任务。在这里,我们将容器提升为首选工件。所使用的通信机制在某些情况下提供一致的组件间通信,充当应用程序编程接口API)。容器的使用提供了一个抽象层,确保与任何运行时语言的兼容性。

与单一的单体应用程序对比,后者通常有紧密耦合的组成组件。紧密耦合意味着将各种模块拆开或创建新的集成会变得非常困难。由于单体结构,语言运行时通常在整个单体中保持一致。无法使用不同的运行时语言可能会导致问题,因为无法使用最适合任务的选项。类似地,当遇到特定的性能瓶颈时,扩展性也可能成为问题。

前面图示的应用程序架构模式是至关重要的考虑因素,因为我们将使用容器来进行云运行。这样做时,我们承诺继续构建轻量级且松耦合的功能。这将帮助你巩固对构建过程的理解,以及在完成本章的其余部分时所采取的具体设计方法。

当然,这并不是说微服务适用于每一种场合。有些情况下,"一刀切"的做法并不适用。从务实的角度选择架构和方法有助于设计出更好的应用,并提升设计者的技能。微服务远非易写,并不适用于每一种场合。如果你希望获得我们之前所提到的好处,建模服务、正确的范围和内容对于微服务来说是一个重大挑战。

为了帮助这一过程,考虑微服务设计模式如何满足需求并帮助你构建可扩展的解决方案通常是非常有帮助的。正如你所预期的,这个主题既广泛又多样,已经在许多演讲和书籍中得到覆盖,试图定义一个关于该主题所需基础知识的共识。由于我们将主要处理与 HTTP 相关的通信,我们将快速概述我们需要考虑的事件处理模式。这些模式如下:

  • 异步事件处理模式

  • 同步事件处理模式

这些模型是你将会经历的最相关的通信类型。

异步事件处理模式

虽然平台上没有明确指出,但事实上,你已经对大多数模式十分熟悉。异步通信通常会利用发布者/订阅者模式。在这个模式中,监听者通过它们订阅的事件被激活。此模式中的消息适合一对多关系。在 Google Cloud 上,Cloud Pub/Sub 提供了此服务,它通过定义的主题和订阅者提供信息,所有匹配的事件都由发布者呈现。在这种情况下,像 Cloud Pub/Sub 这样的服务需要一个类似的模型,以提供适用于信息流的异步通信模式。

如果需要批量导向或一对一的通信流,则作业队列模式更为合适。在这种(赢家通吃)模型中,队列机制用来存储信息,而队列消费者决定何时取回信息。

同步事件处理模式

需要注意的是,任何设计方法都无法为每种情况创造完美的解决方案。以这种方式泛化代码可能会导致内容/代码的重复,这有时是不可避免的。关注保持微服务的隔离性和独立性应是首要任务,你需要接受始终会有例外情况。对于同步事件处理,有两种模式你需要熟悉:

  • 请求/响应模式

  • 蛇形模式

对于同步消息传递,调用服务对即时响应的需求会提供确认。这通常与 HTTP 模型相关,是大多数人熟悉的模式。在这种请求/响应的情境中,消息作为点对点通信的一部分被消费。

管理的另一种状态是观察而非消费同步通信。这被称为侧滑模式,如果有多个端点准备好消费消息,它可能会很有用。然而,只有一个特定的端点地址可以负责生成响应。

在深入讨论 Cloud Run 之前,我们将快速浏览容器并解释为什么它们是一个关键技术。为了讨论这一主题,我们将专注于 Docker 容器;然而,值得知道的是,其他容器也存在,并且提供类似的好处。

使用容器

虽然应用程序可以在任何地方运行,但在不同环境中工作传统上会导致一致性方面的问题。从一个环境部署代码到另一个环境时,常常会遇到由于变化导致与底层基础设施不兼容的问题。行业对从单体应用转向小型集成组件(即微服务)的关注,通常会导致考虑生成松耦合的工件。

基于虚拟化硬件的传统开发提供了一个成熟的平台,在这个平台上有许多成功的部署。然而,部署微服务组件的低效性意味着这种方法由于底层资源的重复使用而变得不那么具有吸引力:

在前面的图示中,可以看到,对于虚拟化硬件,每次虚拟机调用都需要为操作系统和库复制资源。尽管虚拟机继续为大规模机器提供优势,但对于基于微服务的架构,更轻量级的方法是更理想的。

容器通过共享资源模型提供对底层硬件的访问。这种允许主机硬件在容器之间共享现有资源的方法更具吸引力。通过使用容器,主机可以将其资源分配给将在此环境中执行的容器。如果你正在使用微服务的环境,那么这个环境很可能是希望提供容器相关的效率。

持续构建这些组件只是故事的一半;如何确保工件在每次部署中保持一致?为此,我们使用容器来定义一个软件包,在其中可以控制环境(例如,内存、磁盘、网络、文件系统等)。底层云环境利用现有的文件系统创建一个分区,专门为你的容器执行隔离。因此,与其将应用程序直接安装到主机上,不如安装容器并在主机上运行它。由于应用程序存在于主机中,任何不兼容性很可能是与平台相关的。这样一来,你的应用程序现在可以在部署之间保持一致性。

在接下来的章节中,我们将快速概述 Docker 以及如何与 Google Cloud 一起使用它。在本讨论中,我们将覆盖基础内容,以便那些不熟悉容器的朋友能够迅速了解。

利用 Docker

实现容器的最常见方式之一是使用 Docker 容器运行时。Docker 提供了容器的所有优点。它提供了一个简单的界面,可以用来管理应用程序在容器内运行时的状态。为了简洁起见,我们将重点讨论在 Linux 环境中使用 Docker。然而,请记住,其他选项也存在,并且同样有效。

通常,容器有三个主要元素需要考虑。首先,容器使用一个基础镜像来运行应用程序。基础镜像代表应用程序将运行的操作系统,例如,Debian、Ubuntu 或 Alpine。此外,基础镜像的依赖包也需要安装,以确保环境能够与应用程序兼容。这些包通常是与容器兼容的库,例如 SSL、cURL 和 GCloud SDK。最后是命令执行,它表示在执行时会运行什么。添加入口点定义了容器运行时会发生什么。

正如我们之前提到的,容器是隔离应用程序功能的绝佳方式。然而,它们也提供了一种优雅的方式来定义应用程序的签名。你可能会想我说的是什么意思。想象一下,我们有一个应用程序在容器内运行。镜像是使用一个文件构建的,这个文件用于定义应用程序应运行的环境。在这种情况下,镜像代表了可执行的包,并包含了应用程序所需的依赖项。通过完成必要的过程,我们将应用程序的需求隔离到一个可传输的环境(容器镜像)中。这样做非常强大。但理论讨论到此为止——我们来构建一些东西。

我们首先从清单开始。清单表示镜像规范,包括要创建的应用程序。此环境包含一个基础镜像(例如,Scratch、Alpine、Ubuntu、Debian 等),通过 FROM 语句表示。选择基础镜像本身就是一个话题,但请注意,镜像越轻量,部署工作负载就越容易。

除了基础镜像,我们还将集成执行任务所需的包和库。如果你正在使用现代编程语言,可能这些信息已经存在,因为它们已经在本地安装(感谢 Node.js)。如果你是在从其他人的应用程序构建镜像,这是关系可能变得复杂的地方。在我们的示例中,我们不会安装任何额外的包;相反,我们将使用基础 Alpine 镜像的现有能力。

最后,在我们的配置中,我们将定义当应用程序容器启动时,镜像应该执行什么操作。ENTRYPOINT 命令表示容器启动时的调用。CMD 标签表示传递给入口点的参数。请注意,此配置用于允许扩展镜像,以便它可以打印其他消息。强烈建议阅读有关 ENTRYPOINTCMD 的使用,它们在处理命令时可以节省大量时间。

在此过程结束时,你将拥有一个包含所有这些元素的清单文件。为了我们这个示例,我们可以创建一个简单的 Dockerfile。按照以下步骤进行:

  1. 创建 Dockerfile 清单文件:
FROM alpine 
ENTRYPOINT [ "echo" ] 
CMD [ "Hello, welcome to Docker!" ]

如果我们构建前面的清单,它将根据我们制定的指令,并基于每一行清单内容创建镜像。思考一下前述语句,这对我们意味着什么;我们已经为一个单一应用程序构建了一个基于文件的主机机器。

看一下前面的清单内容,我们可以从中了解应用程序的需求,但对执行过程了解甚少。从清单中,我们可以看到基础镜像(即操作系统)、依赖关系(即库/包)以及要执行的命令(即要运行的应用程序)。下一步是运行构建过程,将这个清单转化为代表文件中信息的镜像。

构建 Docker 镜像是通过命令行管理的。将清单转化为有用的东西意味着我们需要生成一个镜像。构建过程会逐行处理清单,并将其添加到最终镜像中。在构建过程中,每一行会创建一个归档层,表示执行的命令。通过之前提供的示例清单,我们可以为我们的应用程序构建一个镜像。

  1. 构建 Dockerfile 清单文件:
 docker build -t hello-docker:1.0 .

在上述命令中,我们告诉 Docker 我们希望通过使用动词 build 启动过程来创建镜像。Docker 默认假设当前目录中有一个名为 Dockerfile 的本地文件。如果您希望使用不同的清单命名约定,您可以在命令行中附加 -f [FILENAME],这种情况下,您需要使用以下命令,技术上等同于步骤 2

  1. 构建一个名为 myDockerfile 的清单文件:
 docker build -t hello-docker:1.0 -f myDockerfile .

build 参数后,我们通过使用 -t [label] 命令告诉 Docker 为镜像添加标签。在这个例子中,我们为将要生成的镜像提供了名称和版本。

将所有创建的镜像进行版本管理被认为是一种良好的实践。

最后,我们通过在命令末尾添加一个句号(句点),指示新镜像将在本地目录中查找清单,从而表明本地目录包含源信息。如果需要,您可以将其替换为更具体的目标位置。

运行上述命令将启动用于构建本地清单的 Docker 工具。此过程成功完成后,我们可以通过让 Docker 列出可用镜像来确认新镜像已存在于我们的机器上。

  1. 列出本地存储的镜像:
 docker images

在这里,您可以查看输出的样子:

从生成的列表中,我们将看到我们的镜像已经成功创建,并且现在可以访问。此外,最新的 alpine 镜像已被下载并用作新镜像的基础镜像。恭喜——从清单构建镜像是一项了不起的成就,并且提升了您对多个主题的整体理解。作为参考,容器是运行在 Linux 子系统上的镜像,并共享主机机器的内核。从这一点来看,我们可以看到容器为在主机上运行独立进程提供了一种轻量级的机制。

现在您已经知道如何构建镜像,我们可以继续在主机上运行容器。刚开始使用容器时,一个常见的困惑是区分“镜像”和“容器”这两个术语。作为参考,镜像指的是一个未运行的容器。镜像一旦运行,就变成了一个容器。这两个术语经常互换使用,但现在您知道它们的区别了。请放心,不必为此烦恼。

要在主机上运行容器,我们需要告诉 Docker 启动哪个镜像,并指定应用程序运行所需的参数。最少,我们需要用于在主机上启动容器的 Docker 命令。

  1. 运行镜像:
 docker run hello-docker:1.0 

在这里,您可以查看输出的样子:

在上面的命令中,我们使用动词 run 来告诉 Docker 启动一个镜像。请注意,此时镜像的位置(即本地或远程)并不重要。如果镜像在本地找不到,系统会自动进行远程仓库搜索。最后,如果镜像在本地或远程都不存在,则会返回错误。

请注意,前面的应用程序实际上非常有用。如果你指定了额外的参数,它将打印该参数,而不是命令默认的消息。试试 docker run hello-docker:1.0 "I love working on Google Cloud"

在本节的早些时候,我们构建了我们的镜像,这意味着它应该可以被 Docker 访问。这个过程可以在下面的图示中看到:

在前面的图示中,我们可以看到一个容器,它处于运行状态,并且已分配了容器 ID。

到目前为止,我们已经执行了以下步骤:

    1. 创建了一个清单文件(例如,Dockerfile)

    2. 从清单构建镜像

    3. 运行镜像以创建一个容器

希望这些操作对你来说是清晰的,你可以看到将 Docker 融入你的开发工作流是多么简单。由于容器在宿主机上运行,它与宿主共享资源。为了确认 Docker 容器是否成功启动,你需要使用 Docker 进程命令来列出所有正在运行的容器。

  1. 列出宿主上所有可用的 Docker 进程:
 docker ps -a

ps 命令选项与 Docker 应用程序启动的进程相关。在这个例子中,我们希望查看宿主上所有活跃的容器。能够跟踪宿主上当前正在运行的进程非常重要。在上面的命令中,我们列出了 Docker 命名空间中的所有进程。这样做可以让我们看到宿主上哪些是活跃的,并为我们提供有关宿主上动态进程的宝贵洞察。在本地主机上运行操作并不是没有代价的。持有活跃容器的机器资源状态需要被停止,以恢复机器的整体资源。

回到微服务的话题,在这个例子中,我将信息输出到屏幕上。对于大多数情况,可能更希望不要将状态输出到屏幕上。除非你确实需要输出状态(例如,它是前端 HTTP 应用程序),否则尽量避免通过屏幕提供反馈。将信息直接发送到日志基础设施是一种更具可扩展性的方法。在接下来的章节中,我们将处理更复杂的例子,这些例子需要特定的网络端口暴露,并将信息直接写入日志。在使用容器的初期阶段采用这种最佳实践是一种很好的习惯,它可以最大程度地减少与移除屏幕内容相关的潜在返工。

在释放与容器相关的资源之前,先花点时间查看正在运行的应用程序生成的日志。为此,我们需要使用特定的命令,并插入正在运行进程的实际容器 ID。

  1. 显示与特定容器相关的日志:
 docker logs <CONTAINER ID>

通过这种方式访问容器日志是一个很好的策略,特别是当我们想要调查容器在活动生命周期中的运行情况时。在运行时信息无法直接输出的情况下,可以使用 Docker 来了解应用程序的运行状态,以便解决出现的任何错误。现在我们已经检查了活动容器的属性,接下来我们应该看看如何释放资源。

要停止一个活动容器,我们需要使用一个特定的命令来停止正在运行的进程。在命令行中输入以下命令,将停止在主机上运行的活动容器。

  1. 停止在主机上运行的容器:
 docker stop <CONTAINER_ID>

在前面的命令中,容器标识符是我们之前通过 ps 命令获得的标识符。每当 Docker 请求标识符时,它很可能是在引用这个有用的参考标题,以便将其与活动组件区分开来。一旦容器停止,与我们简单容器示例相关的资源将被释放回主机机器。现在你已经了解了如何构建和调用镜像,我们可以通过引入两个开发者工具来提升生产力:Google Cloud BuildContainer Registry

填充容器注册表

在上一节中,我们提供了对 Docker 和容器的高层次介绍。在本节中,我们将进一步扩展这个话题,探讨 Google 开发者工具及其如何提升开发者生产力。

在继续之前,让我们列出本节的假设条件,因为接下来会涉及一些依赖项。首先,你的环境应该已经配置好使用 GCloud SDK,并且指向 Google Cloud 上的一个有效项目。此外,Docker 应用程序应已安装,并能够构建镜像。

下图展示了一个典型的开发环境:

如你所见,我们并没有使用本地仓库,而是定义了一个基于Google 容器 注册表GCR)的远程仓库。这个远程仓库替代了 Docker Hub 在 Google 项目中的使用,并为我们提供了一个多区域的仓库。在这个例子中,我们将使用一个简单的清单来构建一个小镜像,并填充到 Google Cloud 仓库中。让我们开始吧:

  1. 创建一个 Dockerfile 清单文件:
FROM alpine 
ENTRYPOINT [ "echo" ] 
CMD [ "Hello Container Registry Repo!" ]

从这里,我们可以启动一个本地构建,使用 build 目录中的默认 Dockerfile 来测试清单文件。

  1. 构建 Docker 镜像:
 docker build -t hello-docker:1.1 .

该过程的第一个不同点是根据构建输出填充仓库。手动实现这一过程的方法是将图像打上仓库端点的标识符标签。我们需要应用一个 tag,以指示创建的工件位于 GCR 中。我们通过附加 gcr.io/[PROJECT_ID] 标签来完成这一操作。这将告诉 GCloud SDK 使用美国仓库并指向特定的 Google Cloud 项目。

  1. 给 Docker 图像打标签:
 docker tag hello-docker:1.1 gcr.io/[PROJECT_ID]/hello-docker:1.1

现在,图像已经正确标注,我们可以将图像推送到 Google Cloud 的远程仓库。

  1. 将图像推送到 GCR:
 docker push gcr.io/[PROJECT_ID]/hello-docker:1.1

在这一点上,本地存储的图像将被推送到 GCR,因此可以远程访问。如果我们希望在本地检索该图像,则需要使用 pull 命令。需要注意的是,身份验证访问使用 IAM 来控制访问(即使您将仓库设为公开)。

  1. 从 GCR 拉取图像:
 docker pull gcr.io/[PROJECT_ID]/hello-docker:1.1

看看前面的示例,显而易见,这个过程与构建和使用 Docker Hub 仓库非常相似。存储在本地的图像需要占用存储空间,这意味着在磁盘空间紧张的情况下,将图像托管在远程仓库是一个值得尝试的方案。正如我们之前学到的,Docker 图像非常灵活,而远程仓库的便利性提供了更灵活的部署策略。

查看远程仓库以及如何填充容器注册表让我们了解到,这个过程涉及一些额外的步骤。幸运的是,Google 创建了一个名为 Cloud Build 的多功能工具,帮助我们减少这些工作量。

使用 Cloud Build

为了增强构建过程,像 Cloud Build 这样的工具可以帮助我们构建更复杂的脚本来创建图像。Cloud Build 是一款开发者工具,尽管它不常被广泛关注,但它对于卸载和自动化一些琐碎的任务(如构建图像)非常有帮助。在图像创建方面,构建的图像将存储在 Google Container Registry 中,并保存在项目相关的仓库中。这些仓库中存储的信息可以单独声明为公开或私有,这为管理构建过程生成的图像提供了简单而有效的方式。

Cloud Build 非常容易集成到您的开发工作流程中。该软件包被描述为一种与语言无关的清单,用于编写所需的自动化流程脚本。Cloud Build 的一些关键特性如下:

  • 原生 Docker 支持

  • 支持多个仓库(例如 Cloud Source Repositories、Bitbucket 和 GitHub)

  • 自定义管道工作流

  • 定制化包支持(例如 Docker、Maven 和 Gradle)

  • 本地或云端构建

  • 包漏洞扫描

现在,我们将使用其中一些工具来构建我们的镜像,并将它们添加到托管在 Google Cloud 上的远程仓库中。首先,我们将再次更新我们的示例清单,并通过命令参数修改消息的输出。让我们开始吧:

  1. 创建 Docker 清单文件:
FROM alpine 
ENTRYPOINT [ "echo" ] 
CMD [ "Hello Cloud Build!" ]

当使用 Cloud Build 时,我们不再直接从命令行调用 Docker。相反,它使用 GCloud SDK 命令在远程仓库上创建镜像作为构建工件。默认的 Dockerfile 需要本地存在,并应作为镜像创建的基础。

  1. 根据 Docker 清单文件启动构建过程:
 gcloud builds submit --tag gcr.io/[PROJECT_ID]/hello-docker:1.2 .

Cloud Build 显示价值的另一个选项是,我们还可以创建一个文件来自动化构建过程。创建一个 cloudbuild.yaml 文件允许开发人员指定一系列步骤作为构建过程的一部分。此过程的参数包括一个丰富的功能集,超越了 Docker。强烈建议您闲暇时进行深入研究。在下面的示例中,我们基本上复制了 docker 命令以构建我们的镜像,并告诉它将输出保留在 Cloud Repository 中。images 行表示与构建工件相关联的标签。完成后,将创建一个新版本(即 hello-docker:1.3),并在容器注册表上可用。

  1. 创建 Cloud Build 清单文件:
steps: 
- name: 'gcr.io/cloud-builders/docker' 
  args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/hello-docker:1.3', '.' ] 
images: 
- 'gcr.io/$PROJECT_ID/hello-docker:1.3'

要使用 Cloud Build 构建上述文件,我们需要从命令行运行以下命令。

  1. 使用 Cloud Build 构建镜像并将镜像提交到 Google Container Registry:
 gcloud builds submit --config cloudbuild.yaml

在前面的示例中,我们概述了将 Docker 清单纳入 Cloud Build 的简单方法。有多种方法可以增强此模型,以便可以结合更复杂的选项。就目前而言,这是我们需要涵盖的关于建立 Docker 工作流的全部内容。在加强了我们对 Docker 的一般理解以及与 Google Cloud 相关的一些开发工具之后,我们将转向 Cloud Run。

介绍 Cloud Run

Cloud Run(以及 Cloud Run for Anthos)是一种基于容器的无服务器技术。这里的一个明显优势是,容器化是一种被广泛采用的方法。能够将您的应用程序打包为容器,然后在不需要任何额外工作的情况下迁移到完全托管的无服务器环境,这是一个理想的方案。

在使用任何技术时,理解组成部分总是很重要的。在这方面,Google Cloud 选择基于几种开源技术来构建其技术,社区可以对其进行贡献。经常会低估在不同云提供商之间移动的能力。在开发应用程序时,重要的考虑因素是该产品/服务技术如何适应以及它将获得的支持。

除了在云中运行容器的基本命题外,Cloud Run 还提供了一个完全托管的无服务器执行环境。与 App Engine 和 Cloud Functions 类似,Google 已经完成了基础设施管理的大部分工作。我之所以这么说,主要是因为 Cloud Run for Anthos 的包含,它需要增加一个 Kubernetes(Google Kubernetes Engine)集群。

构建全栈无服务器应用程序现在已成为现实,能够让你利用这些工具和模式的机会触手可及。与其他服务和平台的集成不应需要大量的代码重写。同样,基于标准组件和兼容的架构平台时,跨不同产品和云提供商的迁移不应成为问题。

在我们继续讨论 Cloud Run 之前,我们先来关注一些用于启用这种灵活无服务器环境的关键特性。

gVisor

gVisor 开源项目为容器提供了一个沙盒化的运行时环境。在这个环境中,创建的容器是在用户空间内核中运行的,兼容性是通过使用开放容器倡议OCI)运行时规范实现的。拦截应用程序系统调用提供了一层隔离,以便可以与受控主机进行交互。这种方法的核心原则是限制系统调用的表面面积,以最小化攻击范围。对于容器环境,能够利用内核空间就意味着可以访问主机机器。为了减少这种情况的可能性,gVisor 力求限制这种访问并限制不受信任的用户空间代码。如下面的图所示,gVisor 使用沙盒化技术为应用程序执行提供虚拟化环境:

如我们所见,来自应用程序的系统调用会传递到 gVisor,在这里决定这些调用是否被允许。通过 gVisor 限制系统调用意味着仅在主机内核级别上给予经过验证的访问权限。这样的做法被描述为深度防御,意味着使用多个层次来提供与主机的增强隔离。

建立这样一个环境使我们能够运行不受信任的容器。在这个实例中,gVisor 通过使用独立的操作系统内核,限制了与主机内核的可能交互。OCI 的美妙之处在于,它使这种集成成为可能,并为如 Docker 和 gVisor 这样的解决方案提供了一种优雅的方式,实现无缝的互换。

Knative

为了开始我们的讨论,我们将深入探讨 Knative 提供的功能,然后简要概述项目中的组件。Knative 为开发人员和运维人员提供了紧密集成 Kubernetes 平台的 API。我强烈建议进一步阅读这一领域的内容,以便获得比本书简短概要所能提供的更深入的理解。

Knative 通过提供一系列组件为 Kubernetes 平台提供多方面的解决方案。这些组件负责许多在平台上工作的标准方面,例如部署、路由和扩展。正如你可能预期的,作为与 Kubernetes 相关的东西,这些组件在框架和应用层之间提供兼容性,使其相对容易融入任何设计中:

在前面的图表中,我们可以看到不同的角色参与 Kubernetes 工作流。运维人员通常负责基础设施维护。开发人员则专注于创建驻留在平台上的应用工作负载,并与 API 进行交互。正是在这一层次,Knative 使开发人员能够为其应用提供更高的效率。

讨论 Knative 时,通常将其描述为中间件,因为它位于 Kubernetes 平台和应用之间。在本章的前面,我们介绍了微服务设计模式;Knative 本质上是这种面向无服务器工作负载的方式的完全实现。在这种关系中,两个主要组件对于讨论至关重要,即 Knative Serving 和 Knative Events:

  • Serving 涉及访问控制工作负载与底层 Kubernetes 集群交互的自定义资源定义CRDs)。此服务的支持计算资源将能够扩展至零。请注意,在 Kubernetes 上,这与资源有关,而不是集群。与平台 API 的交互为我们提供了实施更细粒度控制的机会。在这方面,能够控制服务、路由、配置和修订等元素是通过 Knative serving 实现的。每个元素都用于提供特定的状态管理和通过设定规则进行通信。

Knative Serving 能够抽象不同环境和云提供商之间的服务,例如 ingress。通过这样做,应用开发人员与 Kubernetes 之间的交互变得更加直接和简便。

  • 事件遵循生产者和消费者的概念,其中共享活动需要负责。启用生成的工件的延迟绑定使我们能够整合一个松耦合的服务,该服务能够与其他服务进行交互。事件工件列表使用事件注册表,从而允许消费者在无需引用其他对象的情况下被触发。在这方面,事件消费者必须是可寻址的;也就是说,他们必须能够接收并确认消息。

说到 Knative,我简要提一下 Istio,Istio 是一个服务网格,提供策略执行和流量管理等功能。那么,什么是服务网格呢?服务网格代表一个通常部署在 Kubernetes 上的微服务网络。Istio 提供了多个复杂的功能,包括支持整体管理网格网络的指标。对于在 Cloud Run for Anthos 上部署的无服务器工作负载,Knative 与 Istio 一起提供了 Kubernetes 平台的扩展,以实现对微服务架构更细粒度的控制。

这简要概述了低级组件的内容,应能为您提供一些关于底层 Cloud Run 架构的额外背景。接下来的部分,我们将回到 Cloud Run 和 Cloud Run on Anthos 的话题,对这两个产品进行简要比较。

Cloud Run 与 Cloud Run for Anthos

从根本上讲,Cloud Run 是一个无服务器平台,适用于无状态工作负载。对于此解决方案,不需要基础设施管理。或者,您可能有一个现有的 Kubernetes 集群。在这种情况下,所有工作负载都在该环境中运行。此外,您可能需要一些功能,如命名空间、对 Pod 合作放置的控制或额外的遥测。在这种情况下,Cloud Run on Anthos 提供了一个更为周到的选择。在这两种情况下,要部署的工作负载保持不变,因此作为开发人员,相关工作量不会增加,尽管在部署平台上看似存在差异。

为了理解我们在 Cloud Run/Cloud Run for Anthos 中的意思,让我们从一个图表开始。这将帮助我们观察每个技术栈,从而理解工作流程:

在前面的图表中,可以清晰地看出两种 Cloud Run 形式之间有很多相似之处。在通信的前端是一个用于消费 HTTP 流量的网关。在这里,我们可以将流量路由到基础产品。

在图表的开始,我们可以分辨出 HTTP 流量被路由到我们的环境。流量到 Google 环境通常通过Google 前端GFE)路由。对于 Cloud Run for Anthos 流量,基于 Google Cloud 负载均衡器的附加路由配置在网络层激活(并可能有一个 Istio 网关)。

从容器的角度来看,我们可以看到一个关键的区别。在这一层次上,工件的管理有明确的依赖关系,平台的优先级就是基于这些依赖关系来决定的。这是用于运行对象的计算平台的一个核心区别。在 Kubernetes 上,部署过程使用 Google Kubernetes EngineGKE)。正如我们之前讨论的,所部署的容器工件使用 OCI 来提供运行时和镜像规范。为了访问 Google 的更广泛服务,使用 Knative Serving API 来与 Google API 通信。

我们已经知道,Knative 被用来提供一个可移植和可扩展的 API,支持不同运行时环境,以开发无服务器应用程序。利用 Knative Serving API 来提供可移植性并访问 Google 后端 API 是 Cloud Run 的内在特性。不要低估可移植性的力量,无论你是否已经在享受 Kubernetes 的好处,还是仍在犹豫不决;拥有一个核心组件来无缝管理过渡是一个值得欢迎的补充。我们在本章早些时候提到过 Knative 的一些高层次内容;然而,结合这一能力使得我们可以使用一个出色的平台,扩展应用程序以利用编排工作负载的优势。

既然我们已经了解了底层架构,我们知道,Google Cloud 提供这一无服务器架构的背后有许多相互协作的组件。在接下来的几章中,我们将把注意力转向 Cloud Run 和 Cloud Run for Anthos 的具体内容。

总结

在本章中,我们从高层次讨论了 Cloud Run,并介绍了使这一切成为可能的组成部分。就像 Google 的其他无服务器产品一样,Cloud Run 可以伸缩至零,唯一不同的是,这里的部署工件现在是一个容器。利用容器工件带来了额外的好处,因为 Cloud Run 可以在有 Kubernetes 或没有 Kubernetes 的情况下部署。此外,任何语言的运行时都可以使用,这使得它成为一个非常灵活的产品。

熟悉容器环境(例如 Docker)在这里是一个很大的优势,但 Cloud Run 消除了部署代码的许多复杂性。一旦容器成功构建,它就可以被部署。Cloud Run 内建对无服务器请求/响应消息的支持,因此总是有一种简单且一致的方法来开发组件。对于那些之前不熟悉容器的朋友,希望你们现在已经了解足够多,能够利用它们了。

在本章中,我们为使用 Cloud Run 和容器提供了一个共同的基础。无论你是否相信容器是未来的趋势,它们都是一个需要掌握的重要话题。现在我们已经掌握了 Cloud Run 的基础知识,可以进入更有趣的项目了。在下一章中,我们将继续深入研究这个无服务器产品,并构建一些示例项目。

问题

  1. 描述一下单体应用和微服务应用之间的一些区别。

  2. GFE 执行什么功能?

  3. 请列举两种同步事件处理模式。

  4. 使用 Docker 时,ENTRYPOINT 关键字的作用是什么?

  5. 用于构建镜像的 Docker 命令是什么?

  6. 你能说出 Google Cloud 用于镜像管理的产品是什么吗?

  7. Cloud Build 的目的是什么?

  8. 为什么 Knative API 是 Cloud Run 的一个重要组成部分?

  9. 什么是 OCI,它有什么用途?

  10. 你能列举一些支持容器的不同操作系统吗?

进一步阅读

第八章:使用 Cloud Run 开发

在本章中,我们将深入探讨 Cloud Run 的功能集。正如我们在 第七章《介绍 Cloud Run》中所看到的,Cloud Run 允许无状态容器在基于 Google Cloud 的无服务器基础设施上进行部署和运行。在本章中,我们将重点介绍如何使用 Cloud Run 以及如何利用一些可用的开发工具来开发无服务器应用程序。

Cloud Run 是一个更广泛生态系统的一部分,提供了在大规模上构建 web 服务的手段。有趣的是,它也可以在 Kubernetes 生态系统内运行,而无需对工件配置进行任何更改。如果你之前使用过 Docker 或 Cloud Functions,那么支持 Cloud Run 的大部分环境对你来说是熟悉的。在撰写本文时,Cloud Run 刚刚变得普遍可用;然而,Google Cloud 控制台的一些组件仍处于 alpha 或 beta 阶段,因此可能会发生变化。

本章将涵盖以下主题:

  • 探索 Cloud Run 仪表板

  • 使用 Cloud Run 开发

  • 构建 表示性状态转移 (REST) API

  • 开发人员生产力

技术要求

要完成本章中的练习,你需要一个 Google Cloud 项目或 Qwiklabs 帐号。

你可以在本书的 GitHub 仓库中找到本章的代码文件,位于 ch08 子目录下,链接地址为 github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch08

在阅读本书中的代码片段时,你会注意到在一些情况下,代码/输出中的某些行被移除,并用省略号(...)替代。省略号的使用仅仅是为了展示相关的代码/输出。完整的代码可以在之前提到的 GitHub 链接中找到。

探索 Cloud Run 仪表板

Google Cloud 上的 Cloud Run 界面有几个可用的选项。这些选项与构建过程有关,包括构建触发器和先前构建的历史视图等信息。从 Cloud Run 仪表板开始,此菜单选项依赖于在项目内触发的构建。在撰写本文时,该页面正在进行测试,因此随着产品的成熟,预计会有进一步的更改:

如果你不熟悉 Cloud Build 触发器,我们将在本章后面详细介绍它们。目前,你只需要知道它们是一种自动启动构建的方式,并且在使用 Cloud Run 时它们将是关键。

使用 Cloud Run 开发

从最基本的角度来看,Cloud Run 允许基于容器的 HTTP 端点在云中启动和运行。在上一章中,我们学习了如何创建容器以及如何构建一个与该环境兼容的简单应用程序。了解容器可以让我们选择任何我们想要的运行时语言,并围绕我们的用例创建一个工件。此时,我们将利用这个机会构建我们的第一个 Cloud Run 应用程序,以便熟悉环境和产品。

在这个第一个练习中,我们将调用一些现有代码,并重新回顾静态网站示例(参见 第六章,Cloud Functions 实验)。在这里,我们将探讨如何将现有应用程序进行打包。请记住,在这个示例中,应用程序是基于 Node.js 并且包含对等依赖项的。让我们开始吧:

  1. 要启动项目,我们需要使用以下命令从 GitHub 仓库获取代码:
git clone https://github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud-Platform.git
cd ch08

这款最初构建为 Cloud Functions 应用程序的 web 应用程序,现在将过渡到 Cloud Run。首先,我们将查看这两种产品之间的兼容性。需要注意的是,我们不需要更改应用程序,因为它之前就是使用 Functions Framework 构建的。

  1. 我们已经看到,创建容器需要创建一个 Dockerfile 清单。这个示例应用程序运行在 Node.js 上,因此我们可以走捷径,使用一个与这个框架兼容的现有清单模板。采取这种方式意味着我们不需要再去弄清楚哪些软件包是必需的,可以快速将它们集成到我们的应用程序需求中:
FROM node:12-slim
LABEL MAINTAINER Rich Rose

# Create a work directory
WORKDIR /usr/src/app

# Add packages
COPY package.json package*.json ./
RUN npm install --only=production

# Bundle app source
COPY . .

# Export application PORT
EXPOSE 8080
  1. 为了与我们的 web 应用程序建立兼容性,我们需要更新清单,使其能够识别我们所期望的应用程序配置。我们只需要做最少的更改,就能实现与应用程序的兼容:
FROM node:12-slim
LABEL MAINTAINER Rich Rose

# Create a work directory
WORKDIR /usr/src/app

# Add packages
COPY package.json package*.json ./
RUN npm install --only=production

# Bundle app source
COPY . .

# Export application PORT
EXPOSE 8080

# Create start command
CMD ["npm", "start"]

首先,我们需要安装对等依赖项(例如,functions-frameworkpug)。

  1. 要安装对等依赖项,请运行以下命令:
npm install @google-cloud/functions-framework
npm install pug

这样做意味着我们在创建的镜像中已经包含了这些软件包。此外,我们通过使用npm start命令正确地调用了应用程序。除此之外,清单仍然是一个相当标准的 Node 清单。这些更改是必需的,以便镜像我们在通过命令行运行时可能进行的更改。

在前面的示例中,有一点需要指出的是,指定的端口是8080;这是 Cloud Run 应用程序的默认网络端口。截止到本文写作时,我了解到正在进行进一步的工作,以支持使用替代端口。一旦这个功能可用,似乎在部署时指定端口需求将变得更加实际。因此,包含这一行代码提供了一个潜在的好处。

  1. 为了构建镜像,我们将使用 Cloud Build 而不是 Docker。我们这么做是因为我们将利用 Google 生态系统来管理我们的构件。你可以继续使用 Docker 来构建,但你需要标记并上传你的镜像到 Google 容器注册表。作为复习,Cloud Build 允许我们在 Google Cloud 上安全地执行持续集成。这意味着构建过程可以在本地或云端进行。Cloud Build 使用 GCloud SDK 来启动构建并将生成的构件上传到容器注册表。我们可以通过以下命令来构建镜像,其中我们构建一个 Docker 镜像并标记资源:
gcloud builds submit --tag gcr.io/[PROJECT-ID]/hello-cloudrun:1.0

一旦构建成功完成,我们将能够在容器注册表中看到前面命令的输出。将资源保存在仓库中可以为项目提供广泛的共享选项,无论是内部的还是外部的。

  1. 现在容器镜像已存在,它会自动添加到镜像仓库。下一步是部署代码。只需引用我们之前保存在仓库中的构件即可。请注意,访问已保存的镜像时,需要使用给定对象的完整标签:
gcloud run deploy hello-cloudrun --image gcr.io/$PROJECT_ID//hello-cloudrun:1.0 --platform managed --region us-central1 --allow-unauthenticated

一旦应用成功部署,它的响应方式与云函数的部署非常相似。需要注意的关键点是,从云函数到 Cloud Run 的过渡几乎不需要做太多工作,只需使用一个 Dockerfile 清单。在这种情况下,清单非常简单,并且不需要太多额外的考虑就能使其运行。在此时,当我们返回到 Cloud Run 控制台时,我们不仅能看到已部署的应用程序,还能看到一些附加信息。让我们花点时间来探讨这些新信息。

在这一部分,我们简要浏览了 Cloud Run 界面,并了解了各个组件的用途。然后,我们构建了一个简单的容器来渲染屏幕上的信息。通过这样做,我们可以通过创建更具吸引力的示例来发展我们的知识和技能。

在处理容器时,特别是在 Cloud Run 上工作时,能够整合现有应用程序是至关重要的。创建一个既有信息性又有教育性的用例是一项艰巨的任务,因此非常感谢 Google Cloud Run 团队提供的 PDF 示例。如果你之前没有看到过这个示例,我强烈建议你观看 Next 19 无服务器技术会议的视频,因为这些视频完美地展示了 Cloud Run 的简便性和强大功能(参考链接):

www.youtube.com/playlist?list=PLIivdWyY5sqLYz6HIadOZHE9PsKX-0CF8

要删除现有服务,请使用以下命令:

**gcloud run services delete hello-cloudrun --platform managed --region us-central1**

在下一节中,我们将对本节中讨论的例子进行适应,以便加入额外的处理能力。希望这能突出显示 Google Cloud 环境的强大功能和灵活性。

构建表示状态转移(REST)API

提供一个可扩展的 API 为我们提供了将其他软件集成到我们应用中的机会。我们已经看过如何在开发无服务器应用程序时将构建块组合在一起。通过 Cloud Run,我们可以扩展这些知识,构建可扩展的接口,以暴露对应用程序特定部分的访问。如果你来自 GNU/Linux 背景,这一点会显而易见,并且可能会被视为理所当然。对其他人来说,这可能是一个清晰的时刻,在这个时刻,某个应用程序允许你做出超出预期的事情。有时,我们甚至不知道一个实现支持用于某个任务的接口。

在这个例子中,我们将构建一个基本的 API,使用 REST 来演示如何使用 Cloud Run 来满足这一需求。如果你还没有接触过 REST API 这个术语,通常它指的是无状态操作,其中最常见的是 GET 和 POST。该 API 使用这些操作来通过 Web 资源检索和发送信息。

我们的第一个例子将构建一个基本的 REST API,它基于 Cloud Run 来提供对后端数据的访问。它将包括以下组件:

  • 一个基本的 API:用于检索零售数据。

  • 可用商品列表:API 将提供一个代码,并检索相关的对象信息。

从前面的例子中,我们可以清楚地看到,API 为我们提供了一个简单的机制,可以用来检索与数据对象相关的信息。如果我们扩展数据对象,仍然可以访问这些数据,无需对 API 做任何修改——也就是说,将信息访问与数据分离,从而能够独立增强其功能:

从上面的图示中,我们可以看到,已经定义了多个 API 调用来调用各个端点。每个 API 端点都有特定的任务;例如,检索管理报告、处理事务信息和将信息存储到数据仓库。在这里,我们可以看到,业务端点使用的是 POST API 调用,表示将信息(可能是一组过滤信息)发布到端点进行进一步处理。零售财务端点调用使用 GET 方法来拉取交易数据仓库的信息。

以下是一些帮助定义良好实践的通用规则,当构建引人注目的 REST API 时,我们希望介绍的关键原则如下:

  • 基础 URL

  • API 一致性

  • 错误处理

  • API 版本控制

为了更深入地理解背后的理论和通用规则,我们将探讨上述关键原则。这将帮助我们在本章后面设计 API 时。

基础 URL

我们希望基础 URL 对查询名称具有意义。一个好的起点是考虑基础 URL 应该代表什么,以及如何将其建模以代表底层数据。

要求

在开发周期的这个阶段,拥有访问所有数据集合的手段似乎是非常合理的。能够通过相同的 API 调用将集合中的元素隔离开来也显得非常有用。

如果我们考虑这些要求,需求一个直观的基础 URL 以满足我们的需求应该变得更加明显。在接下来的图表中,我们可以看到在两个查询的帮助下,如何使这个 API 工作:

这里,查询执行以下操作:

  • 查询 A 将能够获取集合中存储的所有书籍。

  • 查询 B 将能够从集合中获取特定项目。

上面的图表展示了一种情况,其中基础 URL 涵盖了数据访问的两个用例:

  • /[集体名词]:访问集合;例如,检索所有书籍

  • /[集体名词]/[元素]:访问集合中的一个元素;例如,检索标签为 1234 的书籍

使用集体名词来对基础 URL 进行分类,可以让我们访问整个集合和单个项目。现在,我们需要简化基础 URL;我们接下来的考虑是如何实现更高的一致性 API。在接下来的子章节中,我们将探讨如何使用 Cloud Run 来实现这一点。

实现基础 URL

我们的第一项任务是实现一个基础 URL。幸运的是,在我们的案例中,工作相对简单;然而,它也说明了我们试图表达的观点。让我们开始吧:

  1. ch08 文件夹中创建一个名为 baseURL 的新目录并进入:
npm init --yes
npm install express 
  1. 编辑生成的 package.json 文件,并在脚本部分添加以下行:
...
"start", "node index.js",
...
  1. 创建一个名为 index.js 的新文件,并添加以下代码:
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;

app.listen(port, () => {
  console.log('Pet Theory REST API listening on port', port);
});
app.get('/books', async (req, res) => {
  res.json({status: 'running'});
});
  1. 创建一个名为 Dockerfile 的新文件,并添加以下内容:
FROM node:12-slim
WORKDIR /usr/src/app
COPY package.json package*.json ./
RUN npm install --only=production
COPY . .
CMD [ "npm", "start" ]

在 Cloud Shell 中工作提供了许多容易被忽视的功能。一个特别的功能是当前项目标识符。要在命令行中获取此值,请执行以下操作:

gcloud config get-value project

更好的是,你可以通过以下方式将其分配给环境变量:

PROJECT_ID=$(gcloud config get-value project)

  1. 基于应用程序构建镜像(请注意,$PROJECT_ID 是一个已设置为我的 Google 项目标识符的环境变量):
gcloud builds submit --tag gcr.io/$PROJECT_ID/base-url
  1. 将镜像部署到 Cloud Run。注意此命令返回的 SERVICE_URL
gcloud run deploy base-url --image gcr.io/$PROJECT_ID/base-url --platform managed --region us-central1 --allow-unauthenticated
  1. 一旦应用程序部署完成,我们可以通过以下命令在 Cloud Shell 中测试我们的 API:
curl [SERVICE_URL]/books

从前面的代码可以看到,要实现基本 URL,我们需要选择一个与我们的 API 互补的东西。在此示例中,集合名词 books 也很适合作为基本 URL。

在考虑如何开发 URL 时,值得考虑 API 的使用场景,并应用一些逻辑来确定如何合理地应用模式。现在我们已经定义了基本 URL,接下来可以看看如何为 API 开发一致的接口。

API 一致性

API 一致性的目标是简化对公开信息的访问:

资源 POST GET PUT DELETE
/books 创建新书籍 列出书籍 批量更新 删除书籍集合
/books/1111 无效 显示一本书 无效 删除一本书

在我们的示例中,集合和元素可以通过使用 HTTP 动词来访问,如前表所示。

要求

从前面的信息中,注意到 API 是如何与 HTTP 动词相关联使用的。例如,我们知道以下内容是正确的:

  • POST 事件用于发送数据。

  • GET 事件用于列出(查询)数据。

  • PUT 事件用于批量更新。

  • DELETE 事件用于删除项目。

我们如何将这些功能与我们的书籍 API 一起使用呢?如前表所示,所呈现的 API 提供了一个一致的接口,用于预期的结果。然而,这种实现会是什么样子呢?我们将在下一小节中讨论这个问题。

实现 API 一致性

一致性是所有 API 开发者所追求的目标。为了实现这一目标,我们需要考虑 API 将如何被使用。在我们的示例中,我们将更新 API,提供一个 GET 请求来获取所有书籍以及单本书籍。我们开始吧:

  1. 编辑 index.js 文件。

  2. 添加 HTTP GET ID

app.get ('/books/:id', async(req, res) => {
  res.json({status: 'GET ID'});
});
  1. 添加 HTTP POST
app.post ('/books', async(req, res) => {
  res.json({status: 'POST'});
});
  1. 添加 HTTP DELETE
app.delete ('/books', async(req, res) => {
  res.json({status: 'DELETE'});
});
  1. 添加 HTTP DELETE ID
app.delete ('/books/:id', async(req, res) => {
  res.json({status: 'DELETE ID'});
});
  1. 添加 HTTP PUT
app.put ('/books/:id', async(req, res) => {
  res.json({status: 'PUT'});
});
  1. 通过运行以下命令来更新构建镜像:
gcloud builds submit --tag gcr.io/$PROJECT_ID/base-url
  1. 使用以下命令重新部署镜像到 Cloud Run:
gcloud beta run deploy base-url --image gcr.io/$PROJECT_ID/base-url --platform managed --region us-central1 --allow-unauthenticated 

现在,我们可以使用 curl 命令来测试我们对 API 所做的增强:

HTTP 动词 测试 /books 端点 测试结果 测试 /books/:id 测试结果
GET curl [SERVICE_URL]/books {"status":"GET"} curl [SERVICE_URL]/books/1111 {"status":"GET ID"}
POST curl --data "author=atwood" [SERVICE_URL]/books {"status":"POST"} N/A N/A
PUT N/A N/A curl -X PUT --data "author=atwood" [SERVICE_URL]/books {"status":"PUT"}
DELETE curl -X [SERVICE_URL]/books/1111 {"status":"DELETE"} curl -X [SERVICE_URL]/books/1111 {"status":"DELETE ID"}

从这些变化中,我们可以看到 API 调用现在是一致的,即获取单个书籍或书籍集合的调用都使用相同的接口类型。接口调用之间的共性提供了调用之间的连续性,从而确保 API 开发者能够理解所进行的调用的含义。

现在我们知道了构成 API 的要素,接下来我们来处理错误。

错误处理

在代码中处理错误情况可能相当复杂。虽然有很多 HTTP 状态码,但这并不意味着一个 API 应该使用所有状态码。从很多方面来看,如果只使用其中的一部分来指示发生了错误,反而会更加清晰。

要求

从高层次来看,有三个特定的结果是必须映射的,如下所示:

  • 200: 应用程序按预期工作,一切正常。

  • 400: 应用程序没有按预期工作。发生了客户端错误;也就是说,客户端数据有误。

  • 500: API 没有按预期工作。发生了服务器错误;也就是说,服务器进程有误。

这个列表可以扩展以包括更详细的分析,但前面的消息为进一步的 API 开发提供了一个良好的基础。

错误处理

理解 API 管理的错误类型并保持一致和清晰是非常重要的。在我们的示例中,我们将采用之前映射的三种结果。让我们开始吧:

  1. 修改每个 API 调用,加入 success 状态调用,如下所示:
res.status(200).json("{status: 'PUT'}");
...
res.status(200).json("{status: 'GET'}");
...
  1. 添加客户端和服务器端错误响应状态(请注意,我们没有服务器端资源,因此此代码仅作为示例使用):
app.use (function(req, res) {
  if (some_server_side_test_fails)
    res.status(500).send("Server Error");
  else
     res.status(404).send("Page not found");
});

通过进行前述代码更改,我们可以看到,当返回适当的状态码时,API 调用的状态更加清晰明了。能够实现一个清晰的接口,并且响应一致,使得开发者能够对所提供的代码的使用充满信心。本节的最后部分将介绍如何处理版本控制。

API 版本控制

一个好的经验法则是始终在即将发布的 REST API 签名中包含版本号。为 API 添加版本号有助于管理缺陷并保持向后兼容性。我们可以采取多种策略在 URL 中加入版本控制;然而,一个好的起点是使用序数来表示接口,如以下示例所示:

/v1/books

在前面的例子中,我们将 API 与 v1 版本相关联,这意味着我们可以清楚地看到查询所应用的版本。如果我们引入 v2 API,用户仍然可以清楚地知道正在使用哪个版本,而无需额外的支持。版本变更通常是为了保持向后兼容性或表示底层 API 的变化。为开发者提供更新代码的机会,而不破坏现有的集成,对于任何使用该 API 的人来说都是至关重要的。

要求

我们希望有一个能够让多个版本共存的 API,像这样:

(v1/books or /v2/books/1111)

如果我们需要集成不同版本的 API,这是一种行得通的方法。

使用路由的 Express 版本控制

当然,我们也应该将版本控制作为 API 的一部分。这里,我们需要更新代码以反映该代码基于书籍 API 的 v1 版本,利用 express 提供的一个很棒的功能——路由。让我们开始吧:

  1. index.js 复制到 bookapi_v1.js

  2. 编辑 index.js 文件并添加以下声明:

const apiBooks_v1 = require("./bookapi_v1.js");
  1. 删除所有 app.putapp.getapp.deleteapp.post 函数。你的代码库应该看起来像这样:
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
const apiBooks_v1 = require("./bookapi_v1.js");

app.listen(port, () => {
  console.log('Pet Theory REST API listening on port', port);
});

app.use (function(req, res) {
  if (some_server_side_test_fails)
    res.status(500).send("Server Error");
  else
     res.status(404).send("Page not found");
});

接下来,我们需要更新代码,使其引用新的书籍版本。按照以下步骤操作:

    1. 编辑 bookapi_v1.js

    2. 删除所有 app.listenapp.use 函数,以及 const 端口定义。

    3. 现在文件应该是这样的:

const express = require ('express');

app.put () {...}

app.delete() {...}
...
app.get(){...}
  1. express 变量后添加一个 Router 的定义:
const bookapi_v1 = express.Router();
  1. 将所有 app. 重命名为 bookapi_v1

  2. 在文件底部添加以下引用:

module.exports = bookapi_v1

重新构建镜像并部署。你会注意到需要在 URL 中添加版本号;例如,[SERVICE_URL]/v1/books。现在 API 已经集成到查询中,更容易确认调用的是哪个版本。随着 API 的成熟,它可能会加入新的特性或替换现有的功能。这些变化可能并非总是向后兼容的,这意味着 API 开发者需要了解任何不兼容的地方。如果你希望管理预期并在 API 开发过程中获取功能集,版本管理是一种不错的策略。

此时,你应该对创建自己 API 时需要考虑的事项有很好的理解。在下一部分,我们将学习如何使用 Cloud Run 开发应用。

开发者生产力

在我们的第二个例子中,我们将探讨随 Cloud Run 环境提供的开发工具。创建 PDF 是现在大多数人都会做的事情,它是分享文档的最简单方法之一。许多企业利用服务向客户发送发票或类似的信息。

类似于 Cloud Functions,熟悉环境并建立开发工作流可以提高你的生产力水平。在这一部分,我们还将看看如何集成其他开发者工具,如 Cloud Run、Cloud Build、源代码库和容器注册表,因为这些在 Google Cloud 上开发时能带来极大的帮助。

在我们开始查看示例之前,先花点时间来探索一个典型的开发工作流。对许多开发者来说,他们的工作流大致像下面这样:

  • 代码保存在一个仓库中,例如 GitHub、Bitbucket 或 Source Repository

  • 启动构建过程

Cloud Source Repository

Cloud Source Repository 如果你想要为你的代码拥有一个私有仓库,它非常有用。不过,它也可以用来镜像公共仓库,这样代码资产就可以在你的项目中使用。一旦仓库在 Google Cloud 上被镜像,任何推送的提交将会自动同步到 Cloud Source Repository。这一点很重要,因为它意味着你可以继续使用原始仓库,而不必担心任何维护任务,确保仓库保持同步。

Cloud Build

构建源代码是一个枯燥的活动。不管是小的还是大的组件,过程无非是一个重复的循环。然而,Cloud Build 可以接管这个过程,释放出一些时间。虽然主要的示例集中在构建 Dockerfile 清单上,但 Cloud Build 实际上可以构建许多其他东西(例如命令、gitgogcloudgradlekubectlnpm)。更棒的是,它可以通过使用开源构建器来扩展构建其他内容。社区构建器已经可以用于 Helm、Flutter 和 Android,且列表还在持续增长。

通过对仓库所做的更改触发构建事件,此时会启动一系列预定义的过程。在 Cloud Build 历史页面(目前仍处于 alpha 阶段)中,可以访问到之前的构建记录。该页面包含了一些具有代表性的数据字段,如构建、源、提交、创建时间/日期和构建时长。还有 13 个额外的信息字段以及一个可应用的筛选器,用于生成报告。

触发器页面是魔法发生的地方;如果您以前使用过 Cloud Build,那么这个页面对您来说一定很熟悉。虽然目前处于测试阶段,但该页面为活动和非活动仓库分别指定了两个视图。活动仓库有触发器与之关联,而非活动的工件仓库则没有。一个典型的触发器是 推送到任意分支,这意味着每次向指定仓库中的分支添加新代码时,都会生成一个事件。该事件与构建过程相关联,从而触发各种预定义和自定义处理。我们将在本章稍后讨论更多关于触发器的内容。

最后是设置页面,您可以在此页面查看服务账号管理的详细信息。为了使用 Cloud Build,您需要将服务账号绑定到项目中的权限。通常,进一步访问所需的额外服务账号权限需要使用 IAM。请记住,除了屏幕,绝大多数角色都可以直接从 Google Cloud 控制台的 IAM 部分进行管理。之前,我们回顾了在 Google Cloud 上使用容器的一些核心基础。为了建立我们对这一点的共同理解,在接下来的部分中,我们将从对 Cloud Run 的简单介绍开始,并看看如何将这些知识融入到进一步的示例中。

持续集成示例

在以下示例中,我们将创建一个实例,构建一个开发工作流,利用仓库并展示在 Cloud Build 中创建这样一个工作流的简便性。假设某个代码存在于一个仓库中,并且我们已经为其预定义了一个触发器。每次将代码提交到仓库时,都会向 Cloud Build 发送一个信号,以启动与镜像仓库相关联的事件。在前面的示例中,我们看到支持多种编程语言。在我们的案例中,我们将启动一个在 Cloud Build 环境中定义的操作,这将替代从本地开发机器手动启动构建过程。为了触发自动化构建,我们需要将代码推送到仓库,这在典型情况下也是如此。

我们的应用程序是一个简单的示例;本例的重点将放在工具上,而非代码。在 ch08 目录下,将当前文件夹更改为 node-ci

cd node-ci

在目录中,注意到有一个 cloudbuild.yaml 文件。正是这个文件确保了构建步骤的执行。文件的内容如下:

steps:
- name: 'gcr.io/cloud-builders/npm' 
  args: ['install']
- name: 'gcr.io/cloud-builders/npm' 
  args: ['audit','fix']
- name: 'gcr.io/cloud-builders/docker' 
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/node-ci:0.1', '.']
images:
- 'gcr.io/$PROJECT_ID/node-ci:0.1'
timeout: "600s"

在前面的示例中,我们省略了测试,因为我们没有定义任何测试。不过,如果您希望将测试包括在内,请添加以下行:

- name: 'gcr.io/cloud-builders/npm'

args: ['test']

请注意,在此文件中定义了三个步骤,所有这些步骤都有助于我们的构建过程:

  1. Npm 包安装:安装依赖包

  2. Npm audit fix:执行检查,查看包是否可以自动修复

  3. Docker build:基于 Dockerfile 使用节点应用程序构建镜像

最后一行表示 cloudbuild.yaml 文件的超时设置和构建最大持续时间。截至撰写本文时,设置为 10 分钟。如果我们运行前面的命令,它将基于给定的代码输出一个新的构建镜像。我们可以从命令行手动调用构建器,如下所示:

gcloud builds submit --config cloudbuild.yaml .

然而,我们不希望手动运行 build 命令;它应该在我们将代码推送到仓库时自动运行。

为了确保仓库按我们希望的方式响应,我们需要添加 Google Cloud Build 操作。在 Google Cloud 控制台中,找到 Cloud Build 选项。从弹出的菜单中,选择触发器(Triggers)选项:

如果你以前没有创建过触发器,屏幕将类似于此图形(如果你已经配置了现有触发器,它们将在此屏幕中显示)。

创建触发器会在源仓库和 Cloud Build 之间建立链接。以下图示说明了创建新触发器所需的信息。应用程序的设置高度可定制,但让我们以一个基本案例作为示例。首先,我们需要在仓库中启用 Google Cloud Build。在这个示例中,请注意使用的源仓库是 GitHub。

当你添加应用程序时,系统会请求你授权访问仓库,并选择应授予访问权限的仓库。需要记住的一点是,并非所有仓库都需要对应用程序开放:

现在我们已经授予了访问权限,在 Google Cloud 中,我们可以使用 Cloud Build(Alpha)基于我们刚配置的仓库创建新的触发器事件。使用连接仓库选项,设置应用程序,以便每当分支更新时,触发器将被触发。默认设置基于仓库中的任何分支更新,但这可以修改为标签或拉取请求。此外,构建配置也可以从实现自动检测功能更改为使用 Dockerfile 或 Cloud Build 配置文件(YAML 或 JSON)。

一旦你设置好这个,更新代码以推动更改。你将看到后端代码被自动触发。恭喜!你刚刚通过推送代码,自动创建了一个构件,从而提高了生产力。在下一章,我们将探讨这个工作流程的一个更复杂的例子,涉及多个服务。

总结

在本章中,我们深入探讨了 Cloud Run 的关键方面,并处理了一些常见的使用场景。在此过程中,我们观察到许多关键概念,例如如何将 Cloud Build 和容器注册表开发工具整合到我们的工作流中。对于那些不熟悉这些工具的用户,希望你现在足够了解它们,以便在日常任务中使用它们。基于我们对容器化环境(例如 Docker)的介绍,我们了解了 Cloud Run 如何去除许多关于部署一致且隔离代码的复杂性。一旦容器成功构建,它就可以被部署。与 Google 其他无服务器产品类似,Cloud Run 可以扩展到零。

Cloud Run 内建支持无服务器请求/响应消息,因此开发组件的方式始终一致且简单。此外,添加新的语言运行时可以不受干扰地实现,同时不妥协服务的灵活性。通过利用容器制品,Cloud Run 可以轻松增强并整合 Kubernetes。

在下一章中,我们将继续我们的 Cloud Run 之旅,并探索在 Kubernetes 平台上工作的关键差异。

问题

  1. 什么是基础 URL?

  2. 为什么 API 版本管理很重要?

  3. 在 REST API 中,你预计哪些动词是可用的?

  4. 什么时候应该返回 HTTP 状态码 4xx?

  5. Cloud Build 的最大持续时间设置是什么?

  6. Cloud Build 可以用于 Android 吗?(正确或错误)

  7. Cloud Build 的错误显示在哪里?

进一步阅读

第九章:使用 Cloud Run for Anthos 开发

在本章中,我们将探索如何利用更复杂的工具和服务来提供生产级别的环境管理。Kubernetes 是一个广泛且令人着迷的主题,超出了本书的范围。然而,了解一些背景和关键元素将使你更容易过渡到这一平台。

如前面章节所述,Cloud Run 和 Kubernetes 的介绍将涵盖技术的关键方面。在这方面,如果你对该主题不熟悉,通过第一部分的学习应该为你提供一个关于 Google Kubernetes EngineGKE)的入门知识。使用 Cloud Run for Anthos 可以充分利用 Kubernetes 的许多优势。本章将提供足够的信息帮助你入门。如果你已经熟悉 GKE,那么可以跳过初始部分,直接进入 Cloud Run for Anthos 的具体内容。

本章将涵盖以下主题:

  • 设置身份和策略管理

  • 使用环境监控

  • 创建自定义网络

  • 建立域名

技术要求

要完成本章的实践练习,你需要一个 Google Cloud 项目。

你可以在本书的 GitHub 仓库中找到本章使用的代码文件,路径为 ch09 子目录:github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch09

身份与策略管理

理解 Google Cloud 上的身份和策略安排是大多数用户的主要学习曲线。身份访问管理是一个重要组件,甚至可以单独作为一本书的主题。简而言之,IAM 在项目上提供策略,以赋予与角色相关的权限。

在 Google Cloud 上,管理操作通常通过服务帐户执行。在与 Google Cloud 目录配合使用时,IAM 角色定义了针对各种用户需求的解决方案。

IAM 对象

从高层次来看,Google Cloud 使用由组织、文件夹、项目和资源组成的层次结构来管理访问权限。

  • 组织节点是 Google Cloud 资源的根节点,包含所有的项目和资源。

  • 文件夹是可选的,用于将项目分组到组织下。一个文件夹可以包含项目和其他文件夹。可以使用 IAM 策略来控制对文件夹中资源的访问。

  • Google Cloud 资源始终与项目相关联。Google Cloud 允许你跟踪资源和配额使用情况、启用计费、管理权限和凭证,并启用服务和 API。

在前述列表中定义的层次结构与成员(即用户服务账户)和角色结合使用,以根据定义的访问权限将项目访问权限限制到特定组。

成员

成员账户很重要,因为它们提供对组织的访问权限。可以将成员账户视为提供域访问权限,用于确定在使用 Google Cloud 提供的服务时可以执行的操作。在处理成员账户时,有两种类型需要考虑:

  • 成员角色:通过授予角色来赋予成员权限。角色定义了授予哪些权限。Google Cloud 提供了预定义角色,并且还可以创建自定义角色。

  • 服务账户:这些账户允许我们控制服务器之间的交互。通常用于验证一个服务对另一个服务的访问,并控制服务可以执行的应用操作。Google 上的服务账户通过 gserviceaccount.com 域中的电子邮件地址来引用。

一旦定义了成员账户,下一步是为该成员分配角色。实际上,这就是提供执行项目内操作的权限。

角色

在 Google Cloud 上,角色提供了一种非常灵活的方式来授予对资源的访问权限。在这种情况下,访问权限作为一个范围提供,其中范围从粗粒度(在 Google Cloud 中称为原始角色)到细粒度(在 Google Cloud 中称为自定义角色),具体取决于使用场景。以下列表概述了三种角色类型。在大多数情况下,将使用这些角色类型的混合体来提供所需的访问类型:

  • 原始:在 Cloud IAM 引入之前存在的最基础角色。定义在项目级别,这些角色提供粗粒度的访问控制,例如 Owner、Editor 和 Viewer 角色。

  • 预定义:预定义的 IAM 角色用于提供比原始角色更细粒度的访问控制。每个 Google Cloud 服务都包含预定义角色。这些角色用于映射到某个工作职能,例如 Compute Network Admin、Security Reviewer。

  • 自定义:由用户定义的包含权限和资源的定制角色。

学习上述角色类型将使得在 Google Cloud 上的工作变得更加容易,因为每个定义的项目将遵循我们所概述的结构。在下一部分,我们将查看 GKE,并了解它如何与 Cloud Run 配合使用。

Google Kubernetes 引擎概述

希望你听说过 Kubernetes,并理解这个平台在技术环境部署中的重要性。对于那些不熟悉 Kubernetes 的人,本节将为你提供关键部分的概述。

Kubernetes 是一个容器编排平台,能够使得调度和维护由系统自动完成,而不是由用户手动进行。

在前几章关于 Cloud Run 的内容中,我们讨论了容器的重要性。我们尚未讨论的是,一旦开始使用容器,如何以更适合生产环境的方式(即一致性和可靠性)来协调这一管理。

正如你想象的那样,容器和 Kubernetes 是互补的技术,它们建立了一个可以扩展运行应用的环境。平台本身可以运行在多种 Linux 服务器上,包括 虚拟机VMs)、云实例,甚至裸金属服务器。作为一个开源项目,开发的速度令人惊叹,贡献的质量也同样出色。

要在 Google Cloud 上使用 Kubernetes,我们使用 GKE。这为 Kubernetes 提供了一个托管环境。为了访问该环境,我们使用名为 kubectl 的命令,也称为 Kubernetes 控制命令。配置和维护 Kubernetes 集群超出了本书的范围,但我们将在将工件部署到 GKE 集群时引用底层构件。

了解产品的使用场景可以节省构建解决方案的工作量。此时,值得概述 Cloud Run 与 Cloud on GKE 之间的关键差异(除了对 Kubernetes 的需求)。

区分 Cloud Run 与 Cloud Run for Anthos

Cloud Run for Anthos 提供了许多 Cloud Run 的优势。在下表中,我们列出了一些用户应了解的关键差异:

Cloud Run Cloud Run for Anthos
计费 按使用量计费 配置的集群资源
机器定制 内存 内存、CPU、GPU、网络
URL 和 SSL 自动 HTTPS URL 手动 SSL 证书
身份和策略 公共,调用者 IAM 角色,CICP 公共或内部

尽管前面表格中列出了差异,但这些服务实际上可以通过简单的部署选项轻松部署,无需任何更改。开发可以在 Cloud Run 上开始。如果需要与 Kubernetes 相关的平台资源,可以迁移到 Cloud Run for Anthos。两者之间还有许多共享的功能:

  • 对于任何部署的服务,都可以进行自动扩展(GKE 受使用的集群限制)。

  • 轻松地通过 TCP 端口 8080 运行基于 HTTP 的应用和服务。

  • 基于容器的简单开发者体验,使用清单文件。

  • 选择任何语言或任何可以打包到容器中的库。

  • 在无需配置环境的情况下,使用自定义域名。

现在我们对 GKE 有了大致了解,可以在接下来的章节中应用我们的知识,使用 Cloud Run for Anthos。

使用 Cloud Run for Anthos

如前所述,使用 Cloud Run for Anthos 提供了利用 Kubernetes 许多好处的能力。在本节中,我们将探讨这些功能的一些内容。我们先从创建(即配置)一个具有 Cloud Run 访问权限的 GKE 集群开始。

配置 GKE

Cloud Run for Anthos 需要一个 Kubernetes 集群。从高层次来看,Kubernetes 提供了一个平台,用于管理(或编排)容器。阐述 Kubernetes 的价值超出了本书的范围,但可以简单地说,它是一个非常值得投入时间的技术。

要将代码部署到 Cloud Run for Anthos,假设已有一个 GKE 集群。Cloud Run for Anthos 在部署之前需要一些预先存在的基础设施。在本节中,我们将启动一个集群,然后将我们的应用程序部署到该集群上,以探索 Cloud Run for Anthos 与 Cloud Run 之间的过程差异。

以下示例使用 Cloud Shell 进入命令行并启用项目中所需的服务。

  1. 要配置集群,可以使用Google Cloud 控制台Cloud SDKGCloud)。在本示例中,将使用 Cloud SDK,以便可以查看底层命令。本节中访问的许多功能将提到 beta/alpha,因为在编写本文时,它们仍处于该状态。由于 Cloud Run 使用 Google 容器注册表和 Cloud Build API,因此需要在项目中启用这些服务。可以通过控制台或使用 Cloud SDK 启用相关的googleapis
gcloud services enable container.googleapis.com containerregistry.googleapis.com cloudbuild.googleapis.com
  1. 在集群上启用 Cloud Run 的 beta 版本意味着,当它变为正式发布GA)版本时,命令的通用格式可能会发生变化。如果你使用 Cloud Shell 运行 Cloud SDK 命令,请注意此环境会定期自动更新,以纳入最新的 SDK 更改。其他环境可能需要手动更新,以确保已安装正确的组件版本。在这里,我们将存储将要使用的集群名称和区域,因为此信息既在集群创建时需要,也在build命令中需要:
export CLUSTER_NAME="test-cluster"
export ZONE="us-central1-a"

请注意,通常建议设置一些本地环境变量,用于存储常见参数,作为一种便利措施。确保环境变量采用大写字母格式,使它们在命令行脚本中更加突出。要访问变量的值,只需在变量名之前加上$符号——例如,echo $CLUSTER_NAME 将显示与集群关联的名称。

  1. 对于 Cloud Run for Anthos,额外需要的元素是 Cloud Run 插件。我们将在这里第一次使用环境变量。要引用这个变量,我们在变量前加上$符号,例如,$CLUSTER_NAME。要配置集群,我们使用以下命令来启动部署 Cloud Run 的环境:
gcloud beta container clusters create $CLUSTER_NAME \
--addons=HorizontalPodAutoscaling,HttpLoadBalancing,Istio,CloudRun \
--machine-type=n1-standard-2 \
--cluster-version=latest --zone=$ZONE \
--enable-stackdriver-kubernetes \
--scopes cloud-platform

请注意,集群名称有一定的限制,因此在创建之前请检查所使用的细节是否符合要求。以下是应用的限制;即它们必须匹配正则表达式(?:a-z?),这意味着以下内容:

    • 只允许字母数字和-

    • 以字母开头,以字母或数字结尾

    • 不超过 40 个字符

当集群被创建时,我们需要向命令提供一些参数,如果你之前使用过 GKE,这些参数会很熟悉。一个标准集群需要以下内容:

    • 机器规格—指示每个节点分配的计算机类型的参考

    • 集群版本—表示将分配给 Kubernetes 集群的版本

    • 区域—将创建计算资源的区域

    • 附加组件—提供额外功能的辅助命令

在配置过程中,GKE Cloud Console 选项中有一个更交互式的命令行版本,显示正在执行的操作。然而,除了出于好奇心,实际上没有必要使用这个命令行版本。

  1. 一旦集群配置过程完成,它将表示已创建的配置。作为一个选项,我们还可以在此阶段为项目设置gcloud命令的默认值:
gcloud config set run/cluster $CLUSTER_NAME
gcloud config set run/cluster_location $ZONE
  1. 为了确认集群已成功创建,使用kubectl命令与 GKE 进行交互。Kubectl 是与已创建集群直接交互的主要方式。为了检查节点,我们需要发出以下命令:
kubectl get nodes

前一个命令的输出将显示信息,如部署的 Kubernetes 节点的名称、状态、年龄和版本。在这个上下文中,Kubernetes 集群本质上是我们定义的节点作为一个单元协同工作。

在我们的 GKE 集群中,我们现在拥有了部署 Cloud Run 的基础平台。在接下来的部分中,我们将花一些时间探索如何将代码部署到这个环境中。此外,我们还将查看一些工作流技巧,以帮助提高开发生产力。

GKE 上的自定义网络

在 GKE 上使用 Kubernetes 时,了解它会创建一个自定义网络是很有帮助的。需要特别注意的是,Kubernetes 将利用 IP 地址来协助其所有资源(例如 Pod、节点和服务等)之间的通信。

内部网络

GKE 集群中最小的元素被称为pod。Pod 用于运行容器,数量可以是一个或多个,具体取决于为给定应用使用的模式。Pods 位于网络命名空间内,用于在 Kubernetes 关联的虚拟网络中隔离访问。

Pods 被调度到节点,并且可以与任何其他 Pod 通信,除非特别限制。因此,Pod 之间的通信是默认的,即使它们被动态删除和重新创建。然而,由于 Pod 是短暂的,外部访问需要一个单独的机制,称为服务(service),以提供一致的访问点。服务使用标签,基本上是 Kubernetes 资源的键值对。这样的服务保持一致的 IP 地址和端口,以便从外部访问时,可以将其作为逻辑单元来访问一组 Pods。

外部网络

对于集群外的流量,情况是以不同的方式管理的。Kubernetes 使用三种不同的机制来提供对内部网络资源的访问。

NodePort 和 ClusterIP 提供了通过 HTTP 负载均衡器的访问。Ingress 访问资源需要定义服务,原因如前所述。然后,通信确保流量被引导到标记为该服务的逻辑组。

需要注意的主要一点是,只要流量保持在集群内部,资源之间的通信就可以启用并得到支持。如果需要外部流量,则应该提供服务和某种形式的负载均衡访问。

部署 Cloud Run for Anthos

既然我们已经创建了集群,我们可以开始思考如何部署应用程序。本节的主要重点是涵盖部署应用程序到 GKE 集群的过程。谷歌非常有意识地使 Cloud Run 命令尽可能相似。和前一节一样,我们将构建一个简单的 Node.js 容器,并将其推送到 GKE。

为了展示我们新 GKE 集群的能力,我们将部署 hello-node Node.js 应用程序。我们的 hello-node 应用程序只是简单地将一条消息打印到屏幕上,所以无需担心代码复杂性。实际上,我们已经在仓库中提供了代码,所以让我们从现有的 Cloud Shell 环境中克隆它:

  1. 克隆 GitHub 仓库:
git clone [github repo]/ch09/hello-node

再次提醒,hello-node 应用程序并没有做什么特别的事情,它只是将 Hello World! 输出到 8080 端口。它已经打包好(也就是打包成 cloudbuild.yaml 文件和 Dockerfile),并准备好用于将镜像推送到容器仓库。

  1. 要开始 Cloud Build 过程,运行以下命令:
gcloud builds submit --config cloudbuild.yaml

在构建过程中,我们将使用 Google 容器注册表,而不是 Docker 注册表。注册表本质上是一个存储镜像的位置,可以用来推送和拉取存储的镜像。使用注册表的好处是,它提供了一个集中存储区域,可以共享你的镜像。

默认情况下,存储在容器注册表中的镜像被标记为私有,需要 IAM 权限才能访问内容。或者,镜像也可以标记为公开,这意味着它们可以在不需要额外身份验证的情况下与项目外部共享。

  1. 一旦构建过程成功完成,生成的镜像将存储在当前的 Google Cloud 项目下,并根据 cloudbuild.yaml 文件中指定的标签进行命名。

在这里需要特别注意的是与镜像关联的标签,因为这是在从仓库拉取镜像时引用该工件的方式:

查看 Google Cloud 容器注册表,以查看上一步构建的镜像。选择注册表中的镜像将显示更多详细信息,包括重要的标签引用。

从容器注册表中,我们可以看到镜像,以及应用的主机名和可见性级别(即,私有/公开)。主机名提供了镜像关联的区域,具体如下:

    • gcr.io:当前位于美国,但位置可能会变化。

    • us.gcr.io:美国

    • eu.gcr.io:欧盟

    • asia.gcr.io:亚洲

在构建镜像时,最好选择距离数据最近的位置,以最大化整体性能。原因是,在拉取镜像(即获取过程)时,您希望尽量减少注册表源和目标主机之间的距离。

镜像的可见性可以是项目私有的,也可以是公开的(即,任何人都可以访问)。默认情况下,镜像设置为私有;不过,如果需要,也可以轻松更改可见性。

此时,镜像已经存在于项目注册表中,集群也可以访问它。在部署镜像之前,让我们花一点时间了解一些用于部署的标准命令中的术语:

    • SERVICE:表示要与部署的镜像关联的名称。

    • IMAGE:一个在容器注册表中可用的工件,将作为服务进行部署。

    • CLUSTER-NAME:与先前创建的集群关联的名称,即 $CLUSTER_NAME

    • CLUSTER-LOCATION:这是分配给集群的区域,即 $ZONE

请注意,IMAGE 标签必须与容器注册表中的标签完全匹配(如果有版本号,也要包括)。如果不确定使用哪个标签,请访问 Cloud Console,选择所需的镜像以查看相关标签详情。

部署后的容器将显示服务名称、修订版(即唯一引用)以及流量服务的 URL。到目前为止,我们已经为要运行的服务创建了一个占位符。为了将容器作为命名服务访问,我们需要部署它。在实际操作中,之前的活动很可能是一次性的任务,并且可以轻松自动化。

  1. 要将工件部署到 GKE 集群,我们使用 gcloud run deploy 命令,方式如下:
gcloud run deploy [SERVICE] --image [IMAGE] --platform gke --cluster [CLUSTER-NAME] --cluster-location [CLUSTER-LOCATION]

现在服务已经部署,关于应用程序运行的大量信息突然变得可用。有两个地方值得关注,关于 GKE 上运行的工作负载的信息:

  • 第一个是 Kubernetes 引擎工作负载页面,其中可以查看 GKE 部署的详细信息。如果您对发送到集群的部署或工作负载的状态感兴趣,这里是获取信息的地方。从这里,您可以深入了解部署的各个方面。

  • 第二个是 Stackdriver。在下一节中,我们将更新应用程序,查看这对数据展示的影响,并了解如何在出现问题时跟踪信息。

现在我们对部署有了基础了解,可以考虑如何利用 Google Cloud 提供的工具集来自动化这一过程。

持续部署

如我们所见,与 Cloud Run 相比,启动 Cloud Run for Anthos 的工作量要大得多。如果我们想要部署代码的另一个修订版本会发生什么?我们需要再次走一遍整个过程吗?其实不需要:服务的部署是唯一需要重复的方面。花些时间更详细地探索这一点会很有意思,并且有助于理解在集群中存在多个修订版本时,发生了什么:

  1. 我们之前的镜像是版本 0.1,因此让我们进行一个小的改动(我在响应字符串中添加了 +1),看看这对部署过程以及向集群推出的影响:
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
app.get('/', (req, res) => res.send('Hello World! +1'));
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
  1. 接下来,使用以下命令更新 Container Registry 中存储的镜像:
gcloud builds submit --config cloudbuild.yaml
  1. 最后,我们希望在集群中部署更新后的镜像:
gcloud beta run deploy [SERVICE] --image [IMAGE] --platform gke --cluster [CLUSTER-NAME] --cluster-location [CLUSTER-LOCATION]

第一步是确认新服务是否已成功部署,可以通过查看命令输出进行确认。要获取 Cloud Run 服务的状态,Cloud 控制台的工作负载页面始终会显示更新的状态,并在变更启动时显示相应信息。

不幸的是,每次我们想要部署某些内容时,都需要执行这些操作,实在是有点麻烦。不过不用担心:有一种更实用的方式来部署我们的代码,它允许我们的可靠伙伴——服务账户和 Cloud Build,处理所有繁琐的部分。

在 Cloud Build 设置中,修改 Cloud Run 服务账户(Cloud Run 管理员)和服务账户(服务账户用户)为启用状态。在左侧面板中,选择触发器选项并连接到有效的仓库。

请注意,如果已经建立了与 GitHub 的连接,必须完成此操作才能成功完成设置。

可以为特定分支或所有推送到仓库的推送设置触发器。请注意,触发器类型会尝试自动检测配置文件。在作者的情况下,cloudbuild.yaml 文件被重命名为 cloudbuild.github,这样就很明显应该使用哪个环境文件。

选择创建按钮,触发器将为选定的仓库激活。

  1. 回到源代码,让我们再对源代码进行一次修改,做一个简单的变更,以便可以明显看到已部署版本之间的区别。更新高亮的行,使其反映如下更改:
const express = require('express')
const app = express()
const port = process.env.PORT || 8080;
app.get('/', (req, res) => res.send('Hello World! GitHub Build'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
  1. 最后,我们需要修改 cloudbuild.github.yaml 文件,以优化我们的构建过程。更新后的配置文件将类似于以下所示:
steps:
  # Build the container image
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/hello-node', '.']
  # push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
  args: ['push', 'gcr.io/$PROJECT_ID/hello-node']
  # Deploy container image to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
  args: ['beta', 'run', 'deploy', 'hello-node', '--image', 'gcr.io/$PROJECT_ID/hello-node', '--platform', 'gke', '--cluster', 'test-cluster', '--cluster-location', 'us-central1-a']
images:
- 'gcr.io/$PROJECT_ID/hello-node'
timeout: "600s"

仔细查看前面代码块中列出的构建文件,我们可以看到 Cloud Builds 的灵活性,因为它允许直接启动命令。进一步检查这些命令,突出显示以下活动:

    • 构建容器镜像build 命令本质上是标准的 docker build 命令,它会使用适当的命名约定为镜像打标。

    • 将容器镜像推送到容器注册表:一旦 docker build 过程完成,构建的镜像将被推送到 Google 容器注册表,并使用指定的标签。再次使用标准的 Docker 命令将镜像发送到仓库。

    • 将容器部署到 Cloud Run:打标的镜像被部署到集群中。

这种布局为基于 Docker 的环境提供了一个常见的构建模式。

  1. 要启动构建,我们再次需要从命令行启动构建过程:
gcloud builds submit --config cloudbuild.github

然而,前述的手动步骤不再必要!该代码库现在已直接与 GitHub 关联,因此每当检测到分支更改时,构建过程会自动启动。使用 Cloud Build 可以节省大量精力,只需极少的配置,非常值得花时间去了解它是如何工作的。

在成功构建了一个服务之后,在下一部分我们将快速了解域名以及在使用 Cloud Run 时如何将其应用于 Google Cloud。

应用域名

一旦服务被部署,Google Cloud 提供了许多选项来管理相关的域名。简而言之,域名提供了将服务 IP 地址映射到互联网并使用人类可读的名称访问的能力。因此,我们可以在浏览器中输入 www.google.com 而不是访问 IP 216.58.206.110 —— 我将留给你考虑哪个更容易记住。

注册域名变得越来越容易,许多公司提供购买自己互联网一部分的机会。一旦你拥有了一个域名,你实际上可以将其映射到你的 Cloud Run 服务。

如果你不想购买域名,Google Cloud 集成了三个外部通配符 域名系统 (DNS) 测试网站,这些网站可以与部署的 Cloud Run 服务关联,如下所示:

DNS 服务 信息
nip.io exentriquesolutions.com/
xip.io basecamp.com/
sslip.io github.com/sstephensongithub.com/cunnie/sslip.io

或者,如果你已经拥有一个域名,也可以将其设置并用于替代默认的域名。当你没有运行实时服务时,拥有自己的域名可能不会太重要。然而,一旦你的产品进入生产环境,拥有一个域名看起来就会更加专业。

Cloud Run for Anthos 默认使用的域名实际上是 example.com。当你更改域名时,例如,更改为测试站点,已部署的服务将注册到所选的名称系统中,例如 xip.io。更改 DNS 设置需要使用配置映射来告诉 Kubernetes 如何配置域名:

在接下来的部分,我们将捕获输出到环境变量中。这样做的目的是我们不需要不断地发送 API 请求来确认值。

  1. 获取当前使用 default.com 的服务 URL,例如:
SERVICE_URL=$(gcloud beta run services describe [SERVICE-NAME] --platform gke --cluster [CLUSTER-NAME] --cluster-location [ZONE] --format "value(status.url)")
  1. 获取已部署服务的外部 IP 地址:
ISTIO_INGRESS=$(kubectl get svc istio-ingress -n gke-system -o json | jq -r '.status.loadBalancer.ingress[0].ip')
  1. 使用 Istio Ingress IP 作为外部 IP,应用补丁到配置映射:
kubectl patch configmap config-domain --namespace knative-serving --patch \
'{"data": {"example.com": null, "[EXTERNAL-IP].[DNS-SERVICE]": ""}}'
  1. 更新服务 URL 环境变量:
SERVICE_URL=$(gcloud beta run services describe [SERVICE-NAME] --platform gke --cluster [CLUSTER-NAME] --cluster-location [ZONE] --format "value(status.url)")

gcloud sdkkubectl 命令提供多种格式的信息,包括 JSON。学习如何解析 JSON 是开发命令行脚本时一个非常实用的技能。

恭喜!你已经为你的服务启用了自定义域名。现在,服务将不再显示 default.com,而是会注册到选定的域名。

在下一部分,我们将讨论应用程序的监控,并提供一些有用的信息见解。

使用 Stackdriver 进行故障排除

前面的章节已经讨论了 Cloud Run 的许多关键细节。到目前为止,我们已经在 GKE 上配置了集群,部署了我们的应用程序,并利用 Cloud Build 提高了我们的生产力。在此时,我们可能还需要讨论当出现问题时该怎么办。

Google Cloud 提供了很多环境信息,特别是对于基于计算的资源,如 GCE 和 GKE。幸运的是,对于无服务器工作负载,我们也可以利用这些关键数据点:

  1. 首先,让我们继续使用我们的 hello-node 应用程序,并向代码中引入一个错误。访问 index.js 的源代码,并将以下错误条目保存到代码中:
const express = require('express')
const app = express()
const port = process.env.PORT || 8080;
oops
app.get('/', (req, res) => res.send('Hello World! GitHub Build'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
  1. 在上述代码中,我们故意添加了一个代码缺陷,以便我们可以在 Stackdriver 中跟踪此问题。我们可以再次运行 cloudbuild.github 来为我们完成所有繁重的工作。然而,这次部署任务会失败!要查看失败的详细信息,我们应该进入 Cloud Run 界面并调查现有服务。

  2. 从 Cloud Run 仪表板视图中,我们可以看到一个绿色圆圈和勾号,表示 hello-node 服务已经成功部署并在预期的参数内运行。与此相比,我们最新部署的 hello-node 服务出现了错误,图中间有一个红色圆圈和感叹号。另外,GKE 集群或命名空间详情没有显示,意味着镜像尚未部署到最终目标位置。

  3. 选择带有错误的应用程序以显示服务详情页面上的更多信息,如下所示:

在深入探讨如何进一步了解显示的错误信息之前,我们应当花点时间来重点介绍 Cloud Run 服务详情页以及从该页可以访问到的信息。

每次部署服务时,都会进行大量的信息交换,其中许多操作是在后台完成的。使用 Google Cloud 的一个重要方面是资源相关数据的集中管理。与项目中部署的服务或资源相关的很多信息将被捕获到 Stackdriver 中。幸运的是,Google 的工程师将 Stackdriver 中最常用的元素提取出来,并将其添加到一个便捷的仪表板中,涵盖了 METRICS、REVISIONS、LOGS、DETAILS 和 YAML,具体如下:

让我们逐一探索这些仪表板元素:

  • 度量(METRICS): 度量屏幕提供了应用程序常用的一系列数据,例如请求计数。此外,展示的信息也可以按时间段进行过滤(例如 1 小时、6 小时、7 天或 30 天)。从这个屏幕中,应用程序的状态可以轻松查看,并可以开始调查与服务性能相关的问题。因此,如果你遇到与服务性能相关的问题,例如延迟或瓶颈,这就是查看应用程序属性以了解发生了什么的地方。

  • 修订(REVISIONS): 展示已部署的修订版本概览,包括环境变量和 Cloud SQL 连接情况。

  • 日志(LOGS): 访问服务的日志信息。这里提供的详细信息基于系统日志,因此应用程序捕获的信息也会出现在此处。

  • 详情(DETAILS): 在此页面中,服务连接和认证信息将会共享。

  • YAML: 最后一页提供了与当前查看的服务相关的 YAML 概览。

记住,在写这本书的时候,仪表板功能仍在修订中,因此你可能会看到一些细微(甚至是明显的)变化。

现在,我们已经概述了仪表板的相对功能,接下来我们可以开始解决我们的服务错误。

你可能还记得,我们早些时候更新了应用程序并引入了一个错误。在 Cloud Run 的主仪表板上,我们可以看到我们的服务未成功。

在本章中,我们从一个简单的应用程序开始,然后将其纳入了持续集成构建过程。传统上,我们使用日志来深入了解那些未按标准流程运行的应用程序。由于我们已将构建过程的许多环节交给自动化流程,因此每个阶段的日志也理应集中存储:

  1. 转到 Stackdriver。那些本应在开发者本地可用的日志现在集中存储在一个中央仓库中。

  2. 在 Cloud Run 仪表盘中,选择显示错误的项目,重新进入服务详情页面。

  3. 在这里,选择 LOGS 选项卡,仔细查看日志进程捕获了哪些信息。

请注意,Stackdriver 中的每一条日志项都带有时间戳,并且与此相关的是返回状态更新的某种命令。

通过屏幕上显示的内容,我们可以看到应用程序是通过index.js命令节点执行的。如果你不明白为什么,是因为这与我们在package.json文件中输入的启动命令有关。

在列表的进一步查找中,我们看到提到了/usr/src/app/index.js:5。这表明在index.js的第五行发生了一些有趣的事情。在接下来的日志中,系统显示了一个有趣的提示:在源文件中发现了oops。显然,这不是我们想要的内容,因此我们找到了拼写错误。

  1. 现在我们已经通过日志探索到了有价值的线索来修正我们的应用缺陷,回到源代码并删除index.js文件中第五行的拼写错误。

  2. 将更改后的代码重新提交到一个分支,并再次查看代码是否自动更新以反映所做的更改。

  3. 此时,代码应根据所做的更新成功工作。通过检查显示的文本是否与当前版本源代码中显示的内容相同来确认代码是否正常运行。

希望通过这个示例,调试应用程序的过程变得更加清晰。通过将开发工作流提前融入 Cloud 流程,它使整体集成更加完整。能够利用内建工具,如 Stackdriver,提供了一个更容易的途径来提高生产力。

删除集群

为了完成这一章,最后一个活动是删除集群。删除集群并不是一个常见的操作;事实上,之所以在这里展示,是为了说明这一过程。正如我们现在所知道的,集群包含了所有与 Kubernetes 相关的基础功能。在 GKE 上,我们的集群是受管理的;也就是说,你不需要担心诸如节点创建、TLS 证书创建等低级别的活动。

话虽如此,要删除之前创建的集群,请使用以下命令:

gcloud beta container clusters delete CLUSTER_NAME

为了澄清,在撰写时,cluster delete命令处于测试阶段,因此将来可能会有一些变化。

一旦我们开始删除集群,它会移除所有相关的工作负载,即已部署的容器。要重新启动,需要创建一个新集群以便部署 Cloud Run for Anthos。

摘要

在本章中,我们介绍了 Kubernetes 的高级概念,并探讨了如何使用 Cloud Run for Anthos。如果你的首选平台是 Kubernetes,那么 Cloud Run for Anthos 就是你应该选择的路径。正如我们所看到的,非 Kubernetes 环境与 Kubernetes 环境之间的迁移无需额外配置,因为交付工件是基于容器的。

通过本章内容,我们发现了一种更高效的方式将 Cloud Build 融入开发者工作流。利用 Google 提供的开发者工具是减少构建和部署过程中重复性工作的明智方式。

在下一章中,我们将开发几个 Cloud Run 示例来说明一些关键功能。通过一些示例用例,将有助于说明如何在自己的项目中使用 Cloud Run。

问题

  1. 使用 Cloud Run for Anthos 时可以进行哪些类型的机器定制?

  2. 使用 Cloud Run for Anthos 时,SSL 证书是自动生成的还是手动生成的?

  3. 部署到 Cloud Run for Anthos 时需要哪些平台标志?

  4. 使用 Cloud Run for Anthos 时,服务访问使用哪个端口?

  5. 使用 Cloud Run for Anthos 需要预先配置集群吗?(正确或错误)

  6. 使用 Cloud Run for Anthos 需要哪些附加组件?

  7. 哪个命令用于从命令行管理 GKE 集群?

  8. 什么是 pod?

  9. GKE 如何支持外部流量?

深入阅读

第十章:Cloud Run 实验室

在本章中,我们将探讨一些使用案例,看看如何在Google Kubernetes EngineGKE)上部署 Cloud Run。最常见的使用案例围绕着构建 Web 应用程序、执行部署以及需要持续集成CI)。

本章的重点是展示使用 Cloud Run 时可以实现的广泛使用案例。本章还将展示在使用该平台时,如何执行部署容器所需的许多活动。

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

  • 构建容器

  • 在 GKE 上部署 Cloud Run

  • 创建一个简单的 Web 应用程序

  • GKE 上的 CI

技术要求

为了完成本章中的练习,你将需要一个 Google Cloud 项目或一个 Qwiklabs 账户。

你可以在本书的 GitHub 仓库中找到本章的代码文件,位于 ch10 子目录下,链接为 github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch10

当你浏览本书中的代码片段时,你会注意到,在一些实例中,代码或输出的某些行已被删除,并用省略号(...)替代。省略号仅用于显示相关的代码或输出。完整的代码可以在之前提到的 GitHub 链接上找到。

构建容器

到目前为止,构建应用程序的容器应该是本书中的一个熟悉的活动。大多数情况下,在构建镜像时,我们通常可以依赖现有的知识来确定如何整合运行时语言或软件包。从个人角度来看,我喜欢将应用程序容器化,因为这提供了一种一致且易于理解的接口。从系统更新和其他变化对应用程序产生影响的隔离是维护计算机时非常常见且令人烦恼的一个方面。

此时的假设是,构建镜像已是驾轻就熟,接下来的挑战是部署容器。你的容器实际要做什么,可能会影响运行容器的复杂性。例如,运行一个带图形用户界面的容器(潜在地)会比一个使用命令行界面的容器面临更多问题。在本章中,我们将通过几个示例,介绍如何执行这个过程。

创建服务

我们将使用 Cloud Shell 或本地开发环境(Cloud SDK)来创建下一个示例,这是一个有趣的示例,用于展示如何构建在容器中运行的应用程序。在这个示例中,我们将开发一个简单的应用程序,名为announce(基于命令行工具Boxes),它将在命令行上显示一条消息。

如果你不熟悉 Boxes,它会在文本周围绘制 ASCII 艺术。首先,我们想要创建一个能够调用 Boxes 应用程序来输出一些任意文本的应用程序。

为了开始示例,让我们初始化环境,准备好构建我们的应用程序。

  1. 初始化npm包:
npm init --yes
  1. 编辑package.json以添加start命令:
...
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
  1. 安装npm包:
npm install express
npm install util
npm install child-process
  1. 创建并编辑一个名为index.js的文件:
const {promisify} = require('util');
const exec = promisify(require('child_process').exec);
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;

app.listen(port, () => {
  console.log('Listening on port', port);
});
  1. 将主要功能添加到index.js文件:
app.get('/', async (req, res) => {
  try {
    let message = req.query.message;
    console.log ('Message: ' + message);
    const cmd='echo ' + message + ' | boxes -d boy';
    const {stdout, stderr} = await exec(cmd);
    if (stderr) {
      throw stderr;
    }
    res.status(200).send("<pre>" + stdout + "</pre>");
  }
  catch (ex) {
    console.log(`${ex}`);
    res.status(500);
  }
});

你可以通过在命令行中运行npm start来检查你的应用程序。它不会做任何惊人的事情,但它会给你机会修正代码中可能存在的任何问题。如果你没有在本地安装boxes,输出将类似于以下内容:

Message: undefined

Error: Command failed: echo undefined | boxes -d boy

不用担心——这是预期的,随着我们应用程序镜像的创建,这个问题会被修正。

  1. 创建一个 Dockerfile:
FROM node:12-slim
LABEL MAINTAINER Rich Rose
RUN apt-get update -y && apt-get install -y boxes && apt-get clean
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . .
CMD [ "npm", "start" ]
  1. 构建镜像:
gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/announce-service:1.0

一旦构建过程启动,类似以下内容的输出将显示:

将镜像推送到容器注册表意味着我们可以更好地控制镜像的访问方式和位置。现在镜像已经构建完成,我们可以通过在本地部署它来进行初步的容器测试。

测试公告服务

将容器部署到容器注册表只是完成工作的一部分。接下来,我们需要从存储的镜像中运行容器。Cloud Run 容器运行在8080端口,因此我们应该为从存储库中提取的镜像启用端口映射:

  1. 从命令行运行容器:
docker run -d -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/announce-service:1.0
  1. 测试应用程序的输出:
curl 127.0.0.1:8080
  1. 测试向应用程序发送消息:
curl 127.0.0.1:8080/?message=cloud+run+on+anthos
  1. 停止本地计算机上运行的 Docker 容器:
docker stop $(docker ps -aq)

恭喜你,现已成功部署并测试了应用程序,能够从容器中生成盒子 ASCII 艺术。在下一部分,我们将把应用程序部署到Google Kubernetes EngineGKE)并从集群中访问该服务。

在 GKE 上部署 Cloud Run

现在我们知道如何创建应用程序的镜像,这如何转化为在 GKE 上运行镜像呢?幸运的是,我们已经知道如何在 Cloud Run 上部署容器,这意味着我们已经掌握了在 GKE 上部署所需的大部分内容。

从外部来看,我们通过 Cloud Run 服务 URL 与之交互。内部运行在 GKE 上时,实际上在后台发生了更多的事情。然而,我们将专注于如何在作为服务运行时复制我们的应用程序。

配置 GKE 集群

在我们将应用程序部署到 GKE 上的 Cloud Run 之前,我们需要创建一个 GKE 集群。那么,让我们开始吧:

  1. 在这个示例中,我们使用了标准的集群定义。因此,创建一个名为hos-cluster-1的 GKE 集群:

选择启用“Anthos 的 Cloud Run”选项非常重要。

以下截图表示 Cloud Run for Anthos 已为集群启用。默认情况下,创建的集群启用了监控:

我们还可以使用命令行在 Google Cloud 上创建集群并执行任务。在此实例中,创建集群需要很长的命令,因此我通常通过 Cloud Console 进行此操作。

将要创建的 Kubernetes 集群是标准的,除了集群配置页面底部的复选框。为了在集群中启用 Cloud Run,必须选中此框。

  1. 集群成功配置后,配置kubectl以连接到该集群:
gcloud container clusters get-credentials hos-cluster-1 --zone us-central1-a --project $GOOGLE_CLOUD_PROJECT
  1. 将服务部署到hos-cluster-1 Kubernetes 集群中。请注意,现在我们指定了 GKE 作为平台,以及集群和集群位置:
gcloud run deploy announce-service \
--platform gke \
--cluster hos-cluster-1 \
--cluster-location us-central1-a \
--image gcr.io/$GOOGLE_CLOUD_PROJECT/announce-service
  1. 创建一个名为ANNOUNCE_URL的环境变量,用于存储已部署服务的 URL:
ANNOUNCE_URL=$(gcloud beta run services describe announce-service --platform gke --cluster hos-cluster-1 --cluster-location us-central1-a --format "value(status.url)")

通过 Istio 为 Cloud Run 服务提供外部访问,Istio 部署在另一个命名空间中。已部署的命名空间会显示在 Cloud Console 中。然而,如果你从默认命名空间的命令行执行kubectl get service,则不会看到 Istio 的引用。要从命令行访问 Istio 网关服务,我们需要指明要使用gke-system命名空间。

测试 GKE 服务

我们已经部署了一个服务,但需要知道如何访问它。GKE 上的 Cloud Run 使用 Istio Ingress 来启用外部访问 GKE 集群:

  1. 要显示 Istio Ingress(即在gke-system命名空间中创建的loadBalancer)的详细信息,请使用以下命令:
kubectl get svc istio-ingress -n gke-system
  1. loadBalancer.ingress地址分配给ISTIO_INGRESS环境变量:
ISTIO_INGRESS=$(kubectl get svc istio-ingress -n gke-system -o json | jq -r '.status.loadBalancer.ingress[0].ip') 

在以下截图中,我们可以看到集群的结果。我们需要的信息被标记为istio-ingress

kubectl命令返回的信息可以进行过滤,只显示所需的字段。例如,在此情况下,我们只需要 IP 地址。如果我们想要过滤输出并返回特定信息,可以使用 JQ 过滤出所需的信息。

JQ 是一个轻量级命令行工具,用于处理 JSON 对象。使用此工具,可以像使用sedawk一样操作 JSON 对象。有关此神奇工具的更多信息,请访问stedolan.github.io/jq/

  1. 可以通过访问 Cloud Run 服务页面或使用curl命令访问与 Cloud Run 控制台中显示的 istio-ingress 关联的外部 IP 来测试集群中运行的应用。例如,在我的案例中,它是announce-service.default.35.223.78.170.xip.io
curl -v http://announce-service.default.[EXTERNAL_IP].xip.io

默认命名空间可以在 Cloud Run 的服务详情页面看到,如下所示:

对于已部署的服务,你可能已经注意到与我们服务关联的域名注册为 xip.io。Cloud Run 在 GKE 上使用的默认域名实际上是 example.com。那么,这个地址是如何被分配给已部署的服务的呢?在测试时,可以从默认域名切换到其他域名。Google 目前提供了三个可以与 Cloud Run 服务一起使用的通配符 DNS 测试站点:

  • nip.io

  • xip.io

  • sslip.io

上述网站提供了一种简单有效的方式,用于注册临时域名以进行测试。

应用自定义域名

或者,如果你拥有一个域名,也可以将其设置为默认域名的替代选项。更改为使用免费的 DNS 站点之一需要执行以下操作:

  1. 通过修补集群的 configmap,设置集群使用自定义域名:
kubectl patch configmap config-domain --namespace knative-serving --patch \
 '{"data": {"example.com": null, "[EXTERNAL-IP].xip.io": ""}}'

要确定 GKE 上 Cloud Run 服务的外部 IP,请记住这指的是 Istio Ingress 的外部 IP。一旦 DNS 更新完成,你将看到已部署服务的地址将使用新的 DNS 引用。

除了替代域名,可能还需要为使用的集群加入 HTTPS。对于大多数情况,建议在集群上使用像 Let's Encrypt 这样的服务。请注意,在使用 Google 服务时,可能需要 HTTPS,因此了解如何将其应用到你的环境中,以确保身份验证方法按预期工作是非常重要的。

控制台输出包含服务和入口的引用。已部署的服务 URL 将在 Cloud Run 控制台中可用。外部可用的 IP 仅对负载均衡器类型的 Istio Ingress 可用。请注意,内部地址范围,即 10.0.x.x,对 curl 命令不可访问。

  1. 访问服务 URL 并将其存储在环境变量中:
SERVICE-URL=$(gcloud beta run services describe password-service --platform gke --cluster hos-cluster-1 --cluster-location us-central1-a --format "value(status.url)")
  1. 列出与 gke-system 命名空间相关的 istio-ingress 信息:
kubectl get svc istio-ingress -n gke-system
  1. 获取与 istio-ingress 端点关联的外部 IP:
ISTIO_INGRESS=$(kubectl get svc istio-ingress -n gke-system -o json | jq '.status.loadBalancer.ingress[0].ip')
  1. 使用 Istio Ingress 的外部 IP 来修补端点以进行测试:
kubectl patch configmap config-domain --namespace knative-serving --patch \
 '{"data": {"example.com": null, "[EXTERNAL_IP].xip.io": ""}}'
  1. 接下来,我们需要生成一个数字 SSL 证书。我们可以通过使用 Certbot 工具来实现。Certbot 使用 Let's Encrypt 作为其后端来自动化证书生成。下载 Certbot 应用程序(certbot.eff.org/)并使用它生成数字 SSL 证书:
wget https://dl.eff.org/certbot-auto
chmod a+x ./certbot-auto
./certbot-auto --help
  1. 将 SSL 证书添加到域名,以为该域名提供 SSL 访问:
./certbot-auto certonly --manual --preferred-challenges dns -d '*.default.[EXTERNAL_IP].xip.io'

恭喜!我们已经成功地在 GKE 上的 Cloud Run 部署并构建了一个简单的服务。然而,为了使用它,我们需要添加一些额外的组件。在接下来的章节中,我们将扩展这个示例,说明应用组件是如何集成的。

创建一个简单的 Web 应用

现在我们已经知道如何在 GKE 上构建和部署 Cloud Run,下一步是探索如何将其与其他服务连接,创建一个应用程序。将我们的应用程序部署到 GKE 后,我们现在需要配置 Cloud Storage 和 Cloud Pub/Sub。

对于我们的示例,我们将构建一个表单来调用我们的宣布服务:

  1. 创建一个名为simple-form的新目录:
mkdir simple-form && cd $_
  1. 初始化环境:
npm init --yes
  1. 编辑package.json文件并添加start命令:
…
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
  1. 安装npm包(pugexpress):
npm install pug
npm install express

在以下代码中,我们将调用我们的announce-service:1.0。将服务的 URL 端点替换为在 Google Cloud 上为您的 Cloud Run 部署生成的地址。

要获取已部署服务的列表,请使用以下命令:

gcloud run services list --platform managed

在下表中,列出了用于部署的命令供参考。请注意平台命令,因为它决定了目标系统:

平台 命令
Cloud Run gcloud run deploy SERVICE --platform managed --region REGION
Cloud Run for Anthos (Google Cloud) gcloud run deploy SERVICE --platform gke --cluster CLUSTER-NAME --cluster-location CLUSTER-LOCATION
Cloud Run for Anthos (VMware) gcloud run deploy SERVICE --platform kubernetes --kubeconfig KUBECONFIG-FILE
  1. 创建一个index.js文件,并添加以下内容:
const express = require('express');
const pug = require('pug');
const app = express();
const port = process.env.PORT || 8080;

app.get("/", function(req, res) {
  const pugTemplate = pug.compileFile('./views/index.pug');
  res.status(200).send(pugTemplate({
    service_url: 'http://do.com'}));
});

app.listen(port, () => {
  console.log('Listening on port', port);
});
  1. pug视图创建一个新目录:
mkdir views && cd $_
  1. views目录下创建一个index.pug文件,并添加以下样式内容:
html
head
style.

  input[type=text], select {
    width: 100%;
    padding: 12px 20px;
    margin: 8px 0;
    display: inline-block;
  }

  input[type=submit] {
    width: 20%;
    padding: 14px 20px;
    border-radius: 4px;
  }
  1. 将表单控件添加到index.pug文件中(将ANNOUNCE_URL替换为项目中先前识别的announce-service URL):

body
  div.header Cloud Run for Anthos:
  p
  div.card
    p.
    Add the text you want to see displayed in the boxes application.
    form(action="[ANNOUNCE_URL]/?message" method="get" target="_blank")
      label Message to display: <input type="text" name="message"><br>
      p
      input(type='submit', value='Submit')
  1. 运行应用程序进行测试,查看表单是否调用服务并显示消息:

输入信息并按下提交按钮后,表单将调用announce-service并显示输入的消息,如下图所示:

恭喜!此时,当输入信息时,它将调用与ANNOUNCE_URL相关的服务。由于服务 URL 已经暴露,我们可以通过 Web 表单或其他围绕该服务构建的简单应用程序轻松集成其他应用程序或服务。

现在我们已经看过了与部署 Web 应用程序相关的常规接口,让我们将注意力转向 CI。在接下来的部分中,我们将了解如何自动化构建过程。

在 GKE 上的 CI

正如我们之前看到的,启动 GKE 上的 Cloud Run 相较于 Cloud Run 需要更多的工作。此外,我们还需要在开发周期中进行迭代,以确保构建、测试和部署过程的一致性。

幸运的是,有一个机制可以帮助开发生命周期管理。Cloud Build 用于自动化开发任务,并且可以轻松扩展以自动化与开发代码相关的许多任务。转向使用容器带来了许多好处,但也增加了确保镜像反映最新变化所需的额外工作层。一个典型的生命周期可能如下所示:

  1. 修改代码。

  2. 构建代码。

  3. 测试代码。

  4. 将代码推送到版本控制。

此外,可能还需要应对不同的环境、工具和流程,这些都超出了开发人员管道中典型阶段门的范围。然而,通过使用 Cloud Build,我们可以轻松创建一个 CI 管道,它可以在提交代码后启动构建功能。

定义管道包括三个部分:

  • 服务账户权限

  • 构建触发器

  • Cloud Build 文件

要开始使用 Cloud Build,需要为 Cloud Run 服务账户(Cloud Run Admin)授予一些额外的权限,并且需要启用服务账户(Service Account User):

  1. 可以在 Cloud Build 界面中查看权限状态:

  1. 触发一个信号,指示应启动构建:

创建推送触发器可以针对特定分支或所有推送到仓库的代码进行设置。请注意,触发器类型会尝试自动检测配置文件。

  1. 构建过程步骤在 cloudbuild.yaml 文件中定义:
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/hello-node', '.']
# push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/hello-node']
# Deploy container image to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args: ['beta', 'run', 'deploy', 'hello-node', '--image', 'gcr.io/$PROJECT_ID/hello-node', '--platform', 'managed', '--region', 'us-central1', '--quiet']
images:
- 'gcr.io/$PROJECT_ID/hello-node'
timeout: "600s"

显然,使用 Cloud Build 可以节省大量精力,几乎不需要配置。使用这些类型的开发工具对 Google Cloud 也有很高的回报,因为该工具可以适应运行广泛的软件包。实际上,Cloud Build 项目的社区相当庞大,涵盖了本书范围之外的许多不同的运行时语言。可以说,投资时间去了解它的工作原理是非常值得的。在第十二章中,提供了一个完整的示例,展示了如何将 Cloud Build 融入到这种类型的 CI 管道中,通过 REST API 消费第三方数据

总结

在本章中,我们强调了迁移到 GKE 上 Cloud Run 的初步元素。容器是与 Kubernetes 协作的基础,提供了一个坚实的基础,帮助我们建立经验和专业知识。为了积累平台经验,我们部署了一个简单的 Web 应用,展示了许多需要掌握的常见元素,这些元素是开发 GKE 上的 Cloud Run 应用时不可或缺的。

管理 Kubernetes 环境的额外复杂性不应阻止你使用这个平台。尽管无服务器架构已经提供了本书中提到的许多好处,但拥有一个支持关键基础设施的自愈平台应当能带来更多的安心。当能够在 Cloud Run 和 Cloud Run for Anthos 之间无缝切换时,确保了无论何时需要,都可以访问到这个选择。

本章结束了我们对 Cloud Run 和 GKE 的简要介绍。在接下来的几章中,我们将再次改变节奏。我们将介绍两个案例研究,以说明无服务器架构的强大,并扩展我们在前几章中获得的一些技术。

问题

  1. 使用容器注册表(Container Registry)相比于其他注册表的一个主要优势是什么?

  2. 使用像 $GOOGLE_CLOUD_PROJECT 这样的环境变量何时有用?

  3. Cloud Build 支持哪些触发器类型?

  4. Cloud Build 服务帐户使用哪个域名?

  5. Cloud Build 支持哪些构建配置类型?

  6. 谁提供本章示例中使用的 Certbot SSL 证书?

深入阅读

第四部分:构建无服务器工作负载

在本节中,你将围绕一个虚构的企业 Pet Theory 开发一个无服务器工作负载。这些章节展示了几种技巧,以及无服务器工作负载最常见的用例。

第一个示例展示了如何将开源应用程序集成并扩展其功能以生成 PDF 文档。在这个示例中,你将学习如何集成多个组件来构建无服务器应用程序。

第二个示例对你提出了更高的挑战。在这个用例中,你将通过使用可扩展架构来创建并集成多个服务。开发者生产力将通过 CI 流水线和 Cloud Build 触发器的使用而提高,这些工具可以自动构建和部署组件。

本节包含以下章节:

  • 第十一章,构建 PDF 转换服务

  • 第十二章,通过 REST API 使用第三方数据

第十一章:构建 PDF 转换服务

在前面的章节中,我们讨论了在 Google Cloud 上使用无服务器计算的相对优点。在本章和下一章中,我们将通过案例研究来探讨如何部署解决方案。通过实际操作这些示例,将演示如何使用我们之前讨论的许多技巧。

为了实现这一目标,我们将使用一个基于 Pet Theory 的示例案例研究,这是一家假设的兽医诊所,正在过渡到在 Google Cloud 上使用无服务器技术。

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

  • 设计文档服务

  • 开发文档服务

  • 开发 Cloud Run 服务

  • 保护服务访问

  • 测试文档访问

技术要求

为了完成本章的练习,你需要一个 Google Cloud 项目或 Qwiklabs 账户。

你可以在本书的 GitHub 仓库中找到本章的代码文件,路径为 ch11 子目录,地址是 github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch11

在你阅读本书中的代码片段时,你会注意到在某些情况下,部分代码行或输出已被删除,并用省略号(...)代替。使用省略号的目的是仅展示相关的代码或输出。完整的代码可以在之前提到的 GitHub 链接中找到。

Pet Theory 案例研究概述

对于我们的案例研究,我们将探讨 Pet Theory 兽医诊所。在这个场景中,企业希望过渡到无服务器技术,并提供一些实用方法,以便将 Google Cloud 及其产品融入到业务中。

完整的 Pet Theory 案例研究包含了多个不同的场景,展示了如何使用无服务器技术解决典型的现实世界问题。要查看完整的场景,请访问 Qwiklabs 网站并搜索 Serverless Workshop: Pet Theory Questwww.qwiklabs.com/quests/98)以查看相关实验。

在本章和下一章中,我们将通过两个实验示例来展示无服务器技术的强大和灵活性。在本章的第一个示例中,我们将看看 Pet Theory 如何通过基于文档自动转换的统一文档流程进行迁移。为了开始我们的回顾,我们将概述所提议的架构试图实现的目标以及涉及的组件角色和需求。

设计文档服务

Pet Theory 在现有的文档管理流程中遇到了一些问题。目前,他们在业务中使用多种文档格式,并希望统一规范,采用单一的标准方法。经过一些咨询,他们决定过渡到可移植文档格式PDF),这样他们在向客户和供应商电子发送信息时,仍然可以使用丰富的媒体内容。

就需求而言,Pet Theory 团队决定他们需要一个能够实现以下功能的系统:

需求 组件
存储文档信息 存储
处理请求 处理
确保服务访问安全 安全
处理文档转换 服务

现在我们已经有了应用的高层次需求,我们应该通过描述每个需求来进一步完善我们的理解。

存储文档信息

从前面的信息来看,我们知道存储是我们架构中的关键组件,因为文档需要存储在某个地方。我们还知道,Google Cloud 有许多产品适合这种需求。

通常,存储的明显选择是使用磁盘或共享文件系统将数据持久化。但是,在这种情况下,这种存储方式可能并不是最适合当前情形。总体要求是尽量减少基础设施的维护,而在托管基础设施上创建传统数据库无法满足这一要求。

根据我们对 Google Cloud Storage 的了解,我们知道短期对象存储可以用于临时信息存储。我们还知道,Cloud Storage 提供了一个事件框架,支持与无服务器计算的交互。因此,Cloud Storage 似乎是一个能够满足我们存储需求的良好候选者。

处理处理请求

处理请求可以通过多种方式进行。在这个示例中,我们将构建一个可以按需扩展的解决方案。为此,我们希望将存储需求与处理组件解耦,这意味着我们需要添加一些中间件,能够高效地处理信息。

在我们的应用中,我们已经决定使用云存储,因此我们需要一种机制来异步地与后端处理进行通信。应用需要尽可能自主运行,以减少用户交互和错误发生的可能性。

将 Cloud Pub/Sub 添加到应用程序中使其能够定义一个一致且可扩展的消息传递服务。该服务可以从 Cloud Storage 中获取信息,并将这些信息传播到执行后端处理的组件。在这一点上,你可能会认为 Cloud Tasks 是一个不错的替代方案。然而,Cloud Tasks 的用例表明,它更适用于需要控制执行时机(即速率限制)的场景。在我们的场景中,我们不需要那种执行控制级别。我们需要的是通用的数据摄取和分发,而 Cloud Pub/Sub 产品更适合这种需求。

在使用 Cloud Pub/Sub 时,我们需要提供与主题相关的信息,这些信息是要传输的数据。在我们的应用程序中,这将包含与上传到 Cloud Storage 存储桶的文件相关的信息。为了消费与主题相关的信息,我们还需要创建一个订阅,它会根据向主题添加新消息来接收事件通知。

确保服务访问安全

作为我们需求的一部分,我们还需要实施服务访问的安全权限,以确保只有授权的帐户可以调用新服务。我们对这个服务的假设是,我们可以使用一个帐户来管理必要的权限。在与 Cloud Functions 和 Cloud Run 相关的章节中,我们已经看到这些服务帐户对于非交互式解决方案是多么宝贵。为了实现这一要求,我们可以使用服务帐户,并为其分配与适当角色相关的必要权限。

处理文档转换

为了选择能够执行文档转换的服务,请花点时间回顾 Google Cloud 上可用的无服务器选项的相对优点:

产品 场景 典型用例
App Engine 无服务器 HTTP 应用程序 Web 应用程序
Cloud Functions 无服务器函数和事件 基于事件的函数
Cloud Run 无服务器 HTTP 容器 完全托管的 HTTP 请求-响应容器

除了原始要求外,Pet Theory 团队还希望能够完成以下任务:

  • 以可扩展的方式处理信息

  • 在实际可行的情况下,利用现有代码/应用程序

  • 最小化应用程序维护

  • 尽可能利用公司内部的知识和技能

一位团队成员发现 LibreOffice 应用程序能够处理文档转换为 PDF。将这个应用程序打包成 Docker 镜像,将使团队能够轻松地在项目中复用和标准化集成。

从前面的段落来看,Cloud Run 似乎非常适合当前的需求。正如我们所了解到的,Cloud Run 支持无状态 HTTP 和自定义库。此外,与 Cloud Pub/Sub 等服务的集成意味着它非常适合我们提出的解决方案。一旦初始构建经过测试和验证,该解决方案可以通过利用 Google 开发者工具,如 Cloud Build,进一步增强,以实现持续集成。

做得好!我们已经进行了高层次的需求分析,将其拆解成组件,现在已经掌握了解决方案的精髓。为了确认我们的理解,以下图表展示了我们设计的解决方案:

在这个图表中,我们定义了三个在 Google Cloud 上运行的不同阶段,这些阶段代表了我们之前定义的组件,用于管理文档处理。我们的架构可以通过以下步骤来描述:

  1. 一个源文档上传到系统存储(即一个云存储桶)。

  2. 触发生命周期事件并生成一个新的负载,发送到 Pub/Sub 主题。

  3. 一个 Pub/Sub 订阅轮询新数据通知。

  4. 一个 Cloud Run 服务处理上传的内容并创建 PDF。

请注意,在前面的图表中,主要框体是一般的抽象,用来表示服务隔离。对于我们简单的服务来说,这应该有助于澄清每个处理阶段以及负责的组件。此外,我们还可以看到,大部分处理并不需要创建代码来处理存储事件通知和消息队列数据对象。我们的努力主要集中在创建服务上,我们明智地选择了一个现有的应用程序来处理 PDF 转换。

基于这一讨论,我们现在对提交给我们服务的文档的 PDF 创建过程有了一个大致的了解。在接下来的部分,我们将开始着手开发 PDF 服务的实际要素,以满足需求。

开发文档服务

在前一部分中,我们概述了无服务器应用程序的架构。在这个分析阶段,我们确定了所需的高层次组件,并推测了所需的活动类型。创建文档服务需要创建一个 Cloud Run 服务,以便从 Cloud Pub/Sub 订阅中获取信息。由于我们选择最小化代码开发过程,我们的生产力得到了显著提高。复杂的通知和消息队列的代码已推迟到 Google Cloud 管理的现有机制中。相反,我们将专注于构建满足需求的特定元素,例如 PDF 转换。

在你的 Google Cloud 项目中,打开 Cloud Shell,并确保可以使用Chap11实验室仓库的克隆版本。

存储文档信息

为了启动项目,我们需要为应用程序创建存储。在以下示意图中,我们可以看到该解决方案使用了两个存储桶。第一个存储桶存储文档上传,第二个存储桶存储输出,即处理后的 PDF 文件:

在 Google Cloud 上创建存储桶很简单,此时无论是从控制台还是 Cloud Shell 执行,都应该是一个熟悉的操作。对于本示例,我们将使用 Cloud Shell 来完成任务:

  1. 为上传的文件创建一个多区域存储桶:
gsutil mb gs://$GOOGLE_CLOUD_PROJECT-upload
  1. 为处理后的文件创建一个多区域存储桶:
gsutil mb gs://$GOOGLE_CLOUD_PROJECT-processed

Google Cloud Storage 提供了启用与 Pub/Sub 相关的通知事件的功能。事件通知系统非常强大,将用于为每个存入上传桶的文档生成新消息。

看一下以下架构图,它显示了当数据被添加到上传桶时,我们会自动收到通知。以这种方式将 Cloud Storage 和 Cloud Pub/Sub 结合在一起,提供了一种合理的使用模式,可以在其他项目中使用,以指示数据的可用性:

现在,每当一个文件存入上传桶时,一个新的 Pub/Sub 主题消息将被排队,表示新内容已经可用。

要在现有桶上设置通知机制,我们使用 gsutil 命令。该命令需要知道触发哪种通知,并且还需要创建一个新的主题供上传桶使用。

  1. 当文件添加到上传桶时,创建一个名为 new-doc 的 Pub/Sub 主题通知:
gsutil notification create -t new-doc -f json -e OBJECT_FINALIZE gs://$GOOGLE_CLOUD_PROJECT-upload

在前面的命令中,我们使用了 OBJECT_FINALIZE 命令,这表示一个新对象被提交到 Google Cloud Storage 存储桶。请注意,生成的信息使用 JSON 格式将信息传递给 new-doc 主题。

只有在部署 PDF 服务后,我们才会收到有关已上传文件的通知(因为对于此新主题没有有效的订阅)。

做得好!我们现在已经启动并运行了数据存储和流处理服务。接下来,我们将看到如何使用 Cloud Run 构建 PDF 服务。

开发 Cloud Run 服务

构建一个服务可能既令人生畏又充满挑战。在本节中,我们将通过一个示例展示如何基于现有应用程序构建服务。在此示例中,我们将使用 LibreOffice 来创建 PDF。

需要提到的一点是,如何轻松集成外部应用程序来构建一个简单的服务。应用程序(特别是 Linux)具有很高的灵活性,这意味着它们通常提供了一个很好的方式将这些应用程序集成到解决方案中。例如,Inkscape 可以用来将 SVG 转换为 PNG,而 Calibre 可以将 EPUB 转换为 PDF。在决定开发应用程序之前,最重要的是调查现有的应用程序可以实现什么功能。

回到服务的创建过程,为了构建我们的应用程序,我们将把 LibreOffice 封装在一个 Docker 镜像中。服务的信息通过一个 Cloud Pub/Sub 订阅提供,封装了 Cloud Storage 通知事件。

我们的应用程序将使用 Node.js 构建,并展示如何访问消息队列信息。尽管代码库相对简短,但它演示了如何将外部应用程序集成到 Cloud Run 中。请记住,当探索替代应用程序时,Cloud Run 应用程序应该是无状态的,并且利用 HTTP。

文档服务通过 Cloud Pub/Sub 的订阅接口接收 HTTP,这意味着它提供了一种简单的机制来交换产品之间的数据。此外,数据交换相关的请求-响应生命周期并不需要状态信息。然而,使用 LibreOffice 处理文件时,将使用分配给 Cloud Run 的/tmp目录临时存储输出文件的信息。

为了使 Cloud Run 执行文档转换,我们将使用来自 Cloud Storage 的通知消息,如下所示:

构建 Cloud Run 服务包括一个 Node.js 应用程序来处理信息,并创建一个 Docker 容器。与之前的示例一样,我们从package.json文件开始,并安装包以启用访问额外资源(例如,Cloud Storage 和 Express.js)。

开发服务

首先,我们需要用正确的信息填充配置文件:

  1. 修改package.json文件,添加一个start脚本,如下所示:
...
"scripts": {
 "start": "node index.js",
 "test": "echo \"Error: no test specified\" && exit 1"
 },
...
  1. 添加转换过程使用的包:
npm install express
npm install body-parser
npm install child_process
npm install @google-cloud/storage
  1. 编辑index.js文件,添加所需的引用和额外的代码。将以下包依赖添加到代码文件的顶部:
const {promisify} = require('util');
const {Storage} = require('@google-cloud/storage');
const exec = promisify(require('child_process').exec);
const storage = new Storage();
  1. 用下面的代码替换现有的app.post函数:
app.post('/', async (req, res) => {
 try {
 const file = decodeBase64Json(req.body.message.data);
 await downloadFile(file.bucket, file.name);
 const pdfFileName = await convertFile(file.name);
 await uploadFile(process.env.PDF_BUCKET, pdfFileName);
 await deleteFile(file.bucket, file.name);
 }
 catch (ex) {
 console.log(`Error: ${ex}`);
 }
 res.set('Content-Type', 'text/plain');
 res.send('\n\nOK\n\n');
})
  1. 添加downloadFile函数:
async function downloadFile(bucketName, fileName) {
 const options = {destination: `/tmp/${fileName}`};
 await storage.bucket(bucketName).file(fileName).download(options);
}

下一个函数是我们程序中魔法发生的地方。从以下代码片段中,我们可以看到 LibreOffice 以无头模式(即没有图形界面)被调用来生成 PDF。生成的文件存储在/tmp目录中,以便后续处理。

  1. 添加convertFile函数:
async function convertFile(fileName) {
 const cmd = 'libreoffice --headless --convert-to pdf --outdir /tmp ' +
 `"/tmp/${fileName}"`;
 console.log(cmd);
 const { stdout, stderr } = await exec(cmd);
 if (stderr) {
 throw stderr;
 }
 console.log(stdout);
 pdfFileName = fileName.replace(/\.\w+$/, '.pdf');
 return pdfFileName;
}
  1. 添加deleteFile函数:
async function deleteFile(bucketName, fileName) {
 await storage.bucket(bucketName).file(fileName).delete();
}
  1. 添加uploadFile函数:
async function uploadFile(bucketName, fileName) {
 await storage.bucket(bucketName).upload(`/tmp/${fileName}`);
}

干得好!现在我们有一个能够根据传递给它的对象生成 PDF 的应用程序。下一步是创建一个 Docker 镜像,该镜像将在容器运行时启动应用程序。

部署服务

要为应用程序创建 Dockerfile 清单,请遵循以下步骤:

  1. 将安装 libreoffice 包的定义添加到 Dockerfile 中:
FROM node:12
RUN apt-get update -y \
 && apt-get install -y libreoffice \
 && apt-get clean
WORKDIR /usr/src/app
COPY package.json package*.json ./
RUN npm install --only=production
COPY . .
CMD [ "npm", "start" ]
  1. 在命令行中,构建服务,从清单创建镜像,并将其存储在 gcr.io 中:
gcloud builds submit \
 --tag gcr.io/$GOOGLE_CLOUD_PROJECT/pdf-converter
  1. 一旦构建过程成功完成,部署该服务:
gcloud beta run deploy pdf-converter \
 --image gcr.io/$GOOGLE_CLOUD_PROJECT/pdf-converter \
 --platform managed \
 --region us-central1 \
 --memory=2Gi \
 --no-allow-unauthenticated \
 --set-env-vars PDF_BUCKET=$GOOGLE_CLOUD_PROJECT-processed

上述 Cloud Run 属性概述了典型部署所涉及的关键属性。由于转换过程占用大量内存,因此我们增加了为 Cloud Run 分配的内存。

  1. 创建一个新的环境变量来保存 SERVICE_URL 参数:
SERVICE_URL=$(gcloud beta run services describe pdf-converter --platform managed --region us-central1 --format "value(status.url)")

我们现在已经在 Google Cloud 中构建并存储了一个镜像。该镜像保存在容器注册表中,且可以通过指定额外的内存和运行区域等属性进行部署。特别地,在实例运行时,我们需要指定一个环境变量,该变量用于指定数据输出的桶名称。

作为本次构建和部署部分的最后一步,我们获取分配给已部署 Cloud Run 实例的服务 URL。获取服务 URL 使用户能够通过环境变量访问该服务。稍后我们将在本练习中使用该环境变量。

恭喜——现在 Google Cloud 上已经存在一个能够根据上传的文件自动生成 PDF 的服务。

确保服务访问

服务成功启用后,我们将重点关注访问安全。为此,我们将创建一个新的服务账户,专门负责管理新服务的调用。

回想一下原始设计,服务实际上是通过 Cloud Pub/Sub 而不是用户调用的。由于我们已经串联了一系列事件,我们可以利用这一点来最小化能够启动我们新服务的外部来源。以下步骤展示了如何创建一个服务账户,专门负责调用新的 Cloud Run PDF 服务实例。

创建服务账户

要创建一个服务账户,请遵循以下简单步骤:

  1. 创建一个新的服务账户:
gcloud iam service-accounts create pubsub-cloud-run-invoker --display-name "PubSub Cloud Run Invoker"
  1. 授予服务账户调用 Cloud Run 的权限:
gcloud beta run services add-iam-policy-binding pdf-converter --member=serviceAccount:pubsub-cloud-run-invoker@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com --role=roles/run.invoker --region us-central1
  1. 创建一个环境变量来保存 PROJECT_ID
PROJECT_NUMBER=$(gcloud config get-value project)
  1. 允许项目创建 Cloud Pub/Sub 身份验证令牌:
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT --member=serviceAccount:service-$PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com --role=roles/iam.serviceAccountTokenCreator

恭喜——使用服务账户来管理资源符合 Google Cloud 提供的最佳实践指南。遵循这些简单步骤确保身份和访问权限仅限于需要访问的服务账户。

现在,我们已经将后端服务限制为仅通过服务账户进行调用,我们可以设置消息队列来处理请求。

处理处理请求

最后的开发任务是向解决方案中添加一个订阅。该订阅用于从 Cloud Pub/Sub 上选定的主题中消费信息。像这样的简单机制可以让信息在不同的服务之间轻松流动。

在下图中,我们将 Pub/Sub 的推送端点绑定到 SERVICE_URL,并将函数的调用与我们之前创建的服务账户绑定。幸运的是,在 Google Cloud 上启动这样的处理请求非常简单:

要在现有的 Cloud Pub/Sub 主题上启动订阅,请执行以下操作:

  1. 创建一个新的 Pub/Sub 订阅,并绑定到 SERVICE_URL
gcloud beta pubsub subscriptions create pdf-conv-sub --topic new-doc --push-endpoint=$SERVICE_URL --push-auth-service-account=pubsub-cloud-run-invoker@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com
  1. 现在已经声明了订阅,将信息传递到 new-doc 主题时,Cloud Pub/Sub 会自动将其推送到订阅者:
{
  "message": {
    "attributes": {
      "key": "value"
    },
    "data": "V2VsY29tZSB0byBHb29nbGUgQ2xvdWQgU2VydmVybGVzcwo=",
    "messageId": "123456789012"
  },
  "subscription": "projects/[PROJECT_ID]/subscriptions/[SUBSCRIPTION_ID]"
}

消息对象同时表示订阅和消息。订阅的键/值对定义了在消息队列中使用的项目和订阅 ID。此外,还包括一个消息对象,详细说明了分配给待发送数据的属性、数据和消息 ID 的键/值对。

  1. 在前面的示例中,一旦消息/数据成功检索,端点将使用 base64 解码传递的消息。

PROJECT_IDSUBSCRIPTION_ID 的值需要对正在使用的 Google Cloud 项目有效。

如果你想知道,前面示例中显示的数据值是经过 base64 编码的消息。要编码一条消息,可以使用 base64 应用程序;例如,echo "Welcome to Google Cloud Serverless" | base64

要解码一条消息,请在应用程序命令行中添加-d参数;例如,echo "V2VsY29tZSB0byBHb29nbGUgQ2xvdWQgU2VydmVybGVzcwo=" | base64 -d

HTTPS 请求将传递到预定义的端点,并在收到后确认。如果传递的对象未被确认成功,将启用重试机制,表明该消息需要重新发送。重试过程将由默认的确认截止时间来协调,这意味着端点需要在超时生效前作出响应。

在使用 Cloud Pub/Sub 时,值得注意的是 Google Cloud 无服务器系统(即 App Engine、Cloud Functions 和 Cloud Run)使用推送机制。由于流量控制是自动建立的,客户端只需指示消息处理的成功/失败。

恭喜你——创建 PDF 服务已经使用 Google Cloud 无服务器技术完成设计和开发。这标志着开发阶段的结束,我们现在可以开始测试我们的新服务。

测试文档服务

一旦服务成功配置完成,我们可以开始测试该服务。在开始这一活动之前,让我们先花点时间查看已构建的服务架构:

尽管我们的服务很简单,但其组成部分去除了与信息处理相关的大部分复杂性。使用云存储避免了与数据库打交道的需要,并为存储中内容变化提供了有效的事件通知系统。

一旦数据上传,事件通知会生成一条新的云 Pub/Sub 消息,其中包含上传文件的相关信息。同样,我们不需要做任何处理就能实现这一结果。Topic/Subscription 机制提供了所需的所有消息处理能力。

最终,表示新文件已上传的消息到达我们的 PDF 后端服务。这时我们会看到之前构建的图像被包含在内。当 Cloud Run 执行时,它会接收来自 Cloud Pub/Sub 的有效负载,然后无缝地处理信息。

为什么值得回顾我们所做的工作呢?嗯,这应该表明在我们定义的端到端过程里,我们实际上需要测试的内容非常少。基本上,我们需要确认以下几点:

  • 存储服务使用的是一个托管活动

  • 流处理服务使用的是一个托管活动

  • PDF 服务基于我们的代码

那么,我们该如何测试 PDF 服务呢?

服务的基本测试可以通过检查服务是否在线,以及将文档添加到指定的上传存储桶来进行。

为了测试 PDF 服务是否正常工作,我们将使用 cURL 程序来获取授权令牌:

  1. 首先,测试服务是否已成功部署:
curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" $SERVICE_URL

现在我们知道服务已上线,可以开始测试服务了。请记住,我们已经为上传存储添加了一个 Pub/Sub 通知事件。一旦数据被添加,消息队列将更新,并向服务发送请求以转换数据文件。

  1. 通过上传一些公共的 Qwiklabs 示例文件来测试转换过程:
gsutil -m cp gs://spls/gsp644/* gs://$GOOGLE_CLOUD_PROJECT-upload
  1. 要确认文件是否已被处理,请检查处理过的云存储桶:

在前面的截图中,你会注意到原始文件现在已经转换成了 PDF。花一点时间检查一下 Cloud Storage 中的原始上传文件夹,注意到上传文件夹现在已经为空。那些已经上传的文件都去哪了?嗯,应用程序包含了一个清理元素,它会在文件成功处理后自动删除这些文件。因此,上传文件夹只包含那些尚未转换的文件。

这个场景涉及了很多工作;然而,由于你已经完成了本书中各章节的学习,所呈现的技术现在应该不再陌生。恭喜你成功创建了 PDF 服务——学习如何构建组件服务将使得更复杂的系统随着时间的推移得以开发。

总结

在本章中,我们详细介绍了如何使用 Google Cloud 无服务器技术来实现项目需求。在这个过程中,我们将初步的客户需求分解,并将其与现有的 Google Cloud 产品相匹配。采取这种方法大大缩短了我们的开发周期,并减少了所需的测试量。

在 Google Cloud 上使用无服务器架构为构建一些令人兴奋的应用程序提供了许多机会。正如我们在本章中看到的,设计和开发过程可以非常有回报。在大多数情况下,与系统合作,减少开发的代码量和应用程序的复杂度,都是值得花时间去做的。我们的应用程序示例清楚地展示了如何使用现有的软件包显著提高整体生产力并满足客户需求。希望本章激发了你的想象力,并为你提供了构建下一个伟大应用程序的灵感。

在下一章中,我们将介绍一个更高级的示例,其中多个服务同时运行。

问题

  1. 使用什么命令可以从命令行访问 Google Cloud Storage?

  2. Stackdriver 日志记录在什么情况下最有用?

  3. Docker 清单文件的作用是什么?

  4. Cloud Build 会将它创建的镜像存储在哪里?

  5. curl 命令可以测试 GET 和 POST 请求吗?(对还是错)

  6. 为什么要使用带有 -m 参数的 gsutil 命令?

  7. 调用服务需要什么类型的权限?

进一步阅读

第十二章:通过 REST API 获取第三方数据

在第十一章中,构建一个 PDF 转换 服务一节中,我们学习了如何使用 Cloud Run 创建一个 PDF 服务。在本章的最后,我们将增加复杂性,基于 Pet Theory 案例研究构建一个更复杂的示例。要求是在 Google Cloud 上使用多个服务实现一个无服务器解决方案。

通过本章的学习,您对无服务器工作负载的知识以及实现真实世界企业产品所需的经验将面临挑战。到本章结束时,将创建一个真实世界的应用程序,展示在 Google Cloud 上无服务器工作负载的关键方面。

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

  • Pet Theory 案例研究概述

  • 设计实验室报告解决方案

  • 开发实验室解决方案

  • 电子邮件和短信通信

  • 持续集成工作流程

  • 测试实验室服务

技术要求

为了完成本章的练习,您将需要一个 Google Cloud 项目或一个 Qwiklabs 账户。

您可以在本书的 GitHub 仓库中找到本章的所有代码文件,位于 ch12 子目录下,链接为:github.com/PacktPublishing/Hands-on-Serverless-Computing-with-Google-Cloud/tree/master/ch12

当您浏览本书中的代码片段时,您会注意到,在一些实例中,部分代码/输出行已被删除并用省略号(...)替代。使用省略号仅是为了展示相关的代码/输出,完整代码可在 GitHub 上的前面提到的链接中找到。

Pet Theory 案例研究概述

根据前一章的内容,我们的案例研究基于 Pet Theory 兽医诊所。完整的 Pet Theory 案例研究包括多个不同的场景,展示了如何使用无服务器技术解决典型的真实世界问题。要查看完整的场景,请访问 Qwiklabs 网站,并参考 Pet Theory Quest,以查看相关的实验室。

在本次练习中,Pet Theory 管理团队表达了他们对处理实验室报告所需人工工作量的担忧。目前,来自第三方实验室的临床报告以电子方式接收,然后由内部管理团队手动处理。报告的管理过程实际上是下载信息并将其添加到报告中,随后通过电子邮件或短信与宠物主人沟通。由于兽医诊所越来越成功,涉及该过程的报告数量已变得难以管理。

为了解决与处理实验室报告相关的问题,提出应构建一个概念验证。初步修订应展示如何自动化处理接收到的实验室报告,并分发报告信息。

为了开始我们的审查,我们将概述一下提议的架构应实现的目标以及相关组件的角色和要求。

设计实验室报告解决方案

在此场景中,Pet Theory 团队希望探索如何使用无服务器技术自动化接收和处理实验室结果。目前,实验室报告是以电子方式接收的,然后必须手动发送给客户。根据之前的示例,执行的活动重点是展示如何用最少的开发工作替换现有任务。此外,所使用的组件应该是松耦合的,以便于进一步的增强,且无需大量返工。

根据已声明的需求,Pet Theory 团队已决定他们需要一个能够满足以下要求的系统:

需求 服务
报告整理 报告
消息处理 消息
电子邮件通信 电子邮件
短信通信 短信
测试数据捕获 测试

让我们通过描述每个需求来加深理解。

报告整理

实验室报告通过 HTTP(S) POST 命令直接发送到 Pet Theory 的 Web 端点。接收到的信息采用 JSON 格式,外部应用程序已就传递的消息内容达成一致。

Cloud Run 和 Cloud Functions 都提供了消费 Web 端点的能力。除了消费消息外,报告服务还需要能够将消息传播到下游服务。

消息处理

Pet Theory 已经遇到过类似的实验室需求,该需求在第十一章中有所讨论,构建 PDF 转换服务。在那个例子中,发送者和接收者通过 Cloud Pub/Sub 使用异步消息传递来实现他们的目标。

在这种情况下,实验室报告将被传送到电子邮件和短信服务进行进一步处理。

电子邮件通信

电子邮件服务代表了一项新需求,能够使用现有的电子邮件解决方案与客户沟通。信息将通过 Cloud Pub/Sub 订阅传递给该组件。

短信通信

类似于电子邮件通信,短信组件提供了一种在接收测试结果时与客户沟通的替代方式。

现在,我们对架构的关键组成部分有了共同的理解,构建解决方案所需的要素应该更加清晰。从概览角度来看,我们的解决方案架构可以通过以下步骤描述:

  1. 一份 JSON 格式的实验室报告被提交到 Pet Theory 的 HTTP(S) 端点。

  2. 一个报告服务消费该 JSON 消息。

  3. JSON 消息中的实验信息发布到 Cloud Pub/Sub 主题。

  4. 一个订阅通知被电子邮件服务接收。

  5. 一个订阅通知被 SMS 服务接收。

此外,我们还可以看到以下组件是必需的:

需要注意的是,还有许多其他方式可以解决这里概述的需求。

在前面的图表中,我们定义了多个组件以满足我们的初步需求。记住,Pet Theory 是一个小型组织,因此它不会想要不必要地承担开发成本。

在本示例中,我们将实现多个服务,每个服务将执行特定的任务。因此,在本练习后期,我们将查看如何通过增强它们来测试各个组件,以便在文档中写入状态更新。

基于我们刚刚覆盖的内容,我们现在对处理提交到我们服务的报告的实验报告解决方案有了一个大致的理解。在接下来的章节中,我们将开始查看开发解决方案的实际要素,以满足需求。

开发实验解决方案

本章将涵盖一些高级主题,因此强烈建议先完成前几章中呈现的示例。在之前的章节中,已介绍了 Google Cloud 及其无服务器产品组合,以帮助你完成此过程。

使用你的 Google Cloud 账户,打开 Cloud Shell,并确保可以访问Chap12实验仓库的克隆。

与前面章节不同,本章不会详细介绍某些活动。相反,本章致力于汇总一个骨架解决方案,展示如何构建一个满足要求的应用程序。如果你遇到困难或需要帮助,别忘了可以查阅解决方案目录。

链接到 Google Docs

除了使用像 Stackdriver 这样的集中式服务外,我们还可以引入替代工具。在本节中,我们将演示如何集成 Google Docs(特别是 Google Sheets)。

当你在 Google Sheets 中创建电子表格时,默认情况下,只有创建者有权限读取和写入该表格。为了允许 Cloud Run 应用程序访问该表格,我们需要授予它访问权限。正如你可能猜到的,对于这项任务,我们将使用服务账户。

从 Cloud Shell 中,我们需要检索我们项目的服务账户名称:

gcloud iam service-accounts list --filter="Compute Engine default service account" --format='value(EMAIL)'

现在,我们可以使用这些信息链接到 Google Docs:

  1. 在 Google Sheets 中创建一个新的电子表格(即,docs.google.com/spreadsheets)。

  2. 点击默认电子表格名称无标题电子表格

  3. 将电子表格重命名为实验结果状态

  4. 点击共享按钮,并输入我们之前检索到的服务账户电子邮件地址。

太棒了!我们现在有一个与我们的服务账户项目关联的电子表格。要将新的电子表格与我们的服务连接,我们需要使用SHEET-ID变量。SHEET-ID变量是文档的唯一标识符,可以通过访问 Google Sheet 的 URL 来获取,示例如下:

从前面的图示中,我们可以看到SHEET-ID已被清楚标出。现在,我们需要将该标识符整合到我们的应用程序代码中。

执行以下步骤,以通过 Cloud Run 访问电子表格:

  1. 访问我们创建的电子表格的 URL。

  2. 复制从spreadsheets/d/之后开始,到/edit之前结束的字符串,如前面的截图所示。

  3. 现在,返回 Cloud Shell 并编辑名为pet theory/lab05/common/sheet.js的文件。

  4. SHEET-ID变量替换为从 URL 复制的值。

很好!现在,可以使用我们的 Cloud Run 应用程序访问该电子表格。

报告整理

根据我们之前讨论的实验服务架构,我们知道要创建的第一个服务与报告整理相关。如果我们专注于此处理阶段的需求,可以看到我们需要处理一个从外部来源接收到的 JSON 文件:

在本练习的这个阶段,让我们利用这个机会创建一个 Cloud Pub/Sub 话题,如下所示:

  1. 运行以下命令:
 gcloud pubsub topics create new-lab-report

Cloud Pub/Sub 话题将用于在报告整理服务和后端电子邮件/SMS 服务之间传递实验报告。

  1. 安装实验报告所需的npm包依赖:
 npm install express
 npm install body-parser
 npm install @google-cloud/pubsub
 npm install googleapis
  1. 修改package.json文件,以包含一个start命令:
 "scripts": {
  "start": "node index.js",
   "test": "echo \"Error: no test specified\" && exit 1"
 },
  1. 创建一个index.js文件,内容如下:
const sheet = require('./common/sheet.js');
const {PubSub} = require('@google-cloud/pubsub');
const pubsub = new PubSub();
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
const port = process.env.PORT || 8080;

app.listen(port, () => {
  console.log('Listening on port', port);
});
  1. 将以下内容追加到index.js
app.post('/', async (req, res) => {
  try {
    await sheet.reset();
    const labReport = req.body;
    await publishPubSubMessage(labReport);
    res.status(204).send();
  }
  catch (ex) {
    console.log(ex);
    res.status(500).send(ex);
  }
})
  1. 最后,在index.js中添加以下函数,然后关闭文件进行编辑:
async function publishPubSubMessage(labReport) {
  const buffer = Buffer.from(JSON.stringify(labReport));
  await pubsub.topic('new-lab-report').publish(buffer);
}
  1. 复制包含 Google API 代码的common目录:
 cp -R ../pet-theory/lab05/common/ .

稍等片刻,查看index.js源代码,并注意以下元素:

    • request.body属性表示要处理的实验报告。

    • 此时不需要与实验报告相关的模式。

    • 实验报告的数据被添加到名为new-lab-report的 Cloud Pub/Sub 话题中。

    • 一旦话题发布,服务返回 HTTP 状态码 204(即,成功但没有数据返回)。

    • 如果发生错误,将返回 HTTP 状态码 500(服务器错误)。

除了报告服务的 Node.js 源代码外,我们还需要在相同目录下创建一个 Dockerfile。

  1. 创建如下的 Dockerfile:
 FROM node:12
 WORKDIR /usr/src/app
 COPY package.json package*.json ./
 RUN npm install --only=production
 COPY . .
 CMD [ "npm", "start" ]

此清单的内容在此阶段应该已经熟悉,无需进一步解释。当我们在后续步骤中讨论组件部署时,我们将部署该清单。

恭喜!能够消费外部实验室报告的报告服务的代码定义已准备好并可以部署。一个 Cloud Pub/Sub 已被配置并准备好接收来自外部第三方的信息。

接下来,我们将更仔细地查看电子邮件和短信服务,并回顾与其实现相关的说明。

电子邮件/短信通信

为了简化概念验证,我们的电子邮件和短信服务将使用相似的代码库来演示如何在 Google Cloud 中设计后台服务。虽然这些组件旨在提供独立的服务,但在我们的示例中,我们将使用共享代码来演示它们的功能。在实际应用中,通信组件提供了一个机会,可以为不同的解决方案(例如,电子邮件、机器人、短信、寻呼机等)复制/扩展共享代码库。

我们之前创建的 Cloud Pub/Sub 主题将数据推送到通信组件进行消费。在这个实例中,实验室报告数据对象表示一个 JSON 文件,用于传递实验室结果:

值得指出的是,我们实际上并没有引用实验室报告中传递的数据,也不知道正在使用的内容架构。相反,我们将其视为一个黑盒,因此没有对内容进行数据验证。采取这一措施将减少开发周期后期需要执行的测试级别。

在 Cloud Run 消息处理的背景下,使用 Cloud Pub/Sub 时,将会观察到以下过程:

  • Cloud Pub/Sub 继续负责推送消息。

  • 订阅者负责消费消息。

  • 一个服务与订阅者对齐,接受消息负载。

再次提醒,我们正在使用的技术现在应该已经熟悉,因为这些设计模式在使用 Cloud Pub/Sub 进行资源间交互时是很常见的。

电子邮件

email-service目录开始,我们需要执行几个操作。如果你不确定具体的命令,请参考本章前面的设计实验室报告解决方案部分:

  1. 修改package.json文件,加入一个启动index.js的 node 语句。

  2. 添加expressbody-parsergoogleapis的包依赖。

  3. 创建一个index.js文件,并填入以下代码:

const sheet = require('./common/sheet.js');
const util = require('./common/util.js');
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());

const port = process.env.PORT || 8080;
  app.listen(port, () => {
  console.log('Listening on port', port);
});
  1. 添加index.js文件,并填入以下额外代码:
app.post('/', async (req, res) => {
  const labReport = util.decodeBase64Json(req.body.message.data);
  try {
    await sheet.update('email', labReport.id, 'Trying');
    const status = await util.attemptFlakeyOperation();
    await sheet.update('email', labReport.id, status);
    res.status(204).send();
  }
  catch (ex) {
    await sheet.update('email', labReport.id, ex);
    res.status(500).send();
  }
})
  1. 复制包含 Google API 代码的common目录:
cp -R ../pet-theory/lab05/common/ .

再次,花些时间观察index.js源代码,并注意以下关键要素:

    • request.body属性通过decodeBase64解码 Cloud Pub/Sub 消息。

    • 数据处理使用了console.log语句。

    • 服务返回一个 HTTP 状态码204(即成功,但没有数据返回)。

    • 如果发生错误,将返回500的 HTTP 状态码(即发生了不成功的服务器错误)。

我们还需要一个 Dockerfile 清单,稍后将用于构建 Node.js 应用的镜像。

  1. Node.js应用创建一个 Dockerfile。

很好!我们继续。

短信服务

在短信服务目录中,我们需要执行相同的步骤。如果你不确定具体命令,可以参考本章前面“开发实验室解决方案”部分的内容:

  1. 修改package.json文件,加入一个用于启动index.js的 Node 语句。

  2. expressbody-parsergoogleapis添加包依赖。

  3. 创建一个index.js文件,并填入以下代码:

const sheet = require('./common/sheet.js');
const util = require('./common/util.js');
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());

const port = process.env.PORT || 8080;
  app.listen(port, () => {
  console.log('Listening on port', port);
});
  1. 添加index.js文件,并填入以下附加代码:
app.post('/', async (req, res) => {
  const labReport = util.decodeBase64Json(req.body.message.data);
  try {
    await sheet.update('sms', labReport.id, 'Trying');
    const status = await util.attemptFlakeyOperation();
    await sheet.update('sms', labReport.id, status);
    res.status(204).send();
  }
  catch (ex) {
    await sheet.update('sms', labReport.id, ex);
    res.status(500).send();
  }
})
  1. 复制包含 Google API 代码的common目录:
cp -R ../pet-theory/lab05/common/ .

添加一个 Dockerfile 清单,稍后将用于构建 Node.js 应用的镜像。

  1. Node.js应用创建一个 Dockerfile。

恭喜!电子邮件和短信服务现在已经可用,并准备好进行部署。现在我们已经看过了整体代码库,接下来可以考虑如何高效构建为这个解决方案所需的多个组件。

持续集成工作流

在构建组件时,使用手动方法看似是一个不错的技巧,因为它实现简单。在本章中,我们将自动化这一过程。这样做的原因是需要构建和部署多个代码库,如果手动操作,可能会在过程中出现不必要的错误。

我们将焦点转向一个开发工具,以处理这个重复的构建和部署过程,而不是使用手动构建。在我们的项目中,我们打算使用 Cloud Build 来管理 Google Cloud 上的构建工作流。要使用 Cloud Build,理解实际需要自动化的过程非常重要,因为所需的配置通常只是手动步骤的近似。

在这个项目中,我们的开发人员工作流可以通过以下步骤来定义:

  1. 开发人员使用git提交他们的代码。

  2. 代码提交会通知源代码库。

  3. 当匹配模式被找到时,Cloud Build 会触发。

  4. 如果构建过程失败,开发人员需要修正代码(返回到第 1 步)。

  5. 如果构建过程成功,镜像将被添加到容器注册中心。

  6. 质量保证QA)团队成员随后可以访问新的镜像。

正如你所看到的,这个过程涉及多个步骤,并且在任何一个阶段都可能发生错误。在这个过程中引入自动化可以通过一致性帮助修复任何错误,并使项目的整体维护更加简便。

已定义的步骤提供了一种常见方法,确保软件构建能够以迭代的方式进行。使用如 Cloud Build 这样的开发工具可以确保构建过程保持一致性和灵活性,并且该过程是通过外部构建文件进行管理的。构建文件可以轻松地在团队成员之间共享,并且意味着更新和构建配置可以在版本控制下进行管理。

开发人员是基于代码提交的持续反馈工作流的一部分,这意味着他们控制代码提交,并且可以直接响应任何问题。构建过程的启动基于代码提交,反馈会告知相关组件提交的成功或失败。

看一下下面的图表,了解这个过程在实际操作中是如何工作的:

一旦代码组件成功构建,它将被存储在容器注册表中。此时,QA 团队成员可以访问最新的已验证镜像,并能够独立测试每个镜像,而无需参考开发团队。通过这种方式,替代版本如开发版、预发布版和生产版可以作为不同的阶段门进行部署和测试。

配置实验服务 CI

要了解这个过程如何应用于我们的示例,我们可以基于一个基本的 Cloud Build 脚本为 Pet Theory 创建一个配置。

你可以参考本章结尾的进一步阅读部分,获取关于账户权限的更多信息。

要开始,请返回到lab-service目录。这是我们将创建基本 Cloud Build 配置的地方:

  1. lab-service创建并编辑一个新的cloudbuild.yaml文件:
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/lab-service', 'lab-service/.']
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/lab-service']
# Deploy the image to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args: ['beta', 'run', 'deploy', 'lab-service', '--image', 'gcr.io/$PROJECT_ID/lab-service', '--region', 'us-central1', '--platform', 'managed', '--no-allow
-unauthenticated']
images:
- 'gcr.io/$PROJECT_ID/lab-service'
timeout: "600s"

接下来,我们将为我们的项目设置 Google Source Repositories。我们需要创建一个临时仓库来托管我们的代码,并展示 Google Cloud 的一些附加功能。

  1. 创建 Cloud Source Repositories 配置:
 gcloud source repos create pet-theory-test
  1. 克隆新创建的仓库,请注意你需要提供你的项目身份作为参数:
 gcloud source repos clone pet-theory-test --project=[PROJECT_ID]
  1. 将子目录(即email-servicesms-servicelab-servicecommon)复制到新的 Google Source 仓库目录中。你的新pet-theory-test目录应该像这样:
.
├── common
│ ├── sheet.js
│ └── util.js
├── email-service
│ ├── index.js
│ ├── node_modules
│ ├── package.json
│ └── package-lock.json
├── lab-service
│ ├── cloudbuild.yaml
│ ├── Dockerfile
│ ├── index.js
│ └── package.json
│ └── package-lock.json
└── sms-service
├── index.js
├── node_modules
├── package.json
└── package-lock.json
  1. 检查git目录的状态,确保你有作为未跟踪文件的常见email-servicelab-servicesms-service子目录:
 git status
  1. 将修改后的文件添加到git中:
 git add .
  1. 使用你的电子邮件地址设置电子邮件凭证:
 git config --global user.email "EMAIL"
  1. 使用你的用户名设置名称凭证:
 git config --global user name "MY NAME"
  1. 向仓库添加提交信息:
 git commit -m "Initial commit - Pet Theory CH12"
  1. 将代码直接提交到主分支:
 git push origin master
  1. 在控制台中检查 Cloud Source Repositories,以确保代码现在可用。

恭喜!了解如何使用 Cloud Source Repositories 可以在 Google Cloud 中托管代码时节省时间。通常,能够使用基于 Git 的解决方案能更好地保障代码的安全性。

触发 Cloud Build

现在,既然我们已经将代码放入了 Cloud Source Repositories,如何让它使用 Google Cloud 开发者工具自动构建呢?为了在提交时自动构建代码,我们可以使用我们的老朋友 Cloud Build。为了演示构建过程,我们将使用lab-service目录:

对于本示例,请确保您位于pet-theory-test/lab-service目录中。Cloud Build 的初始步骤需要选择一个代码库。

  1. 从 Cloud Console 中访问 Cloud Build(位于开发工具选项下)。

  2. 选择触发器菜单选项和创建触发器选项。

  3. 使用以下信息设置触发器:

字段
名称 trigger-lab-service
描述 推送到 lab 分支
触发器类型 Branch
分支(正则表达式) ^([lL]ab)/\w+
Cloudbuild 配置 lab-service/cloudbuild.yaml
替代变量 N/A
  1. 选择屏幕底部的创建触发器按钮来启用触发器。

Cloud Build 触发器表中的正则表达式分支是过滤应构建内容的常见方式。在许多情况下,开发者只希望构建特定的分支,使用正则表达式有助于隔离特定的分支。

要测试触发器是否已成功设置,请向lab-service触发器提交代码更改。按照以下步骤操作:

  1. cloudbuild.yaml文件中添加注释:
steps:
# comment
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/lab-service', 'lab-service/.']
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/lab-service']
# Deploy the image to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args: ['beta', 'run', 'deploy', 'lab-service', '--image', 'gcr.io/$PROJECT_ID/lab-service', '--region', 'us-central1', '--platform', 'managed', '--no-allow
-unauthenticated']
images:
- 'gcr.io/$PROJECT_ID/lab-service'
timeout: "600s"
  1. 将更新后的文件添加到git
 git add cloudbuild.yaml
  1. 添加提交信息:
 git commit -m "Add updated cloudbuild.yaml"
  1. 将更改推送到 Cloud Source Repositories:
 git push origin lab/fix-1

在最后一个命令中,我们使用了一个特殊的正则表达式命令,表示该更改与lab-service触发器相关。当我们设置触发器时,我们使用了正则表达式,只查看标记为lab的分支:

一旦代码提交,检查 Cloud Build 历史记录将显示新的构建任务已开始。在每次 Git 提交到一个分支后,我们的组件将自动触发构建过程。现在我们知道构建配置正常工作,让我们将相同的更改推广到电子邮件和 SMS 目录。我们将确保每个构建都保持在单独的分支上,以最小化代码合并的潜在问题。

触发电子邮件和 SMS

根据我们与lab-service触发器一起完成的工作,我们需要设置一个 Cloud Build 触发器。然而,这次我们可以简单地复制现有的lab-service触发器:

在本示例中,我们为服务使用了一个单独的代码库。然而,在实际应用中,这种方法并不理想。相反,我建议为每个服务使用一个独立的代码库,以便更好地隔离代码。

要设置触发器,请执行以下步骤:

  1. 从 Cloud Console 中访问 Cloud Build(位于开发工具选项下)。

  2. 选择三点菜单项以打开弹出菜单,然后选择复制。

  3. 选择(三个点菜单)新创建的触发器项以打开弹出菜单,并选择“编辑”。

  4. 设置触发器并填写以下信息:

字段
名称 trigger-email-service
描述 推送到电子邮件分支
触发类型 Branch
分支(正则表达式) ^([eE]mail)/\w+
Cloudbuild 配置 email-service/cloudbuild.yaml
替代变量
  1. 在屏幕底部选择“保存”按钮以启用触发器。

  2. 现在,完全按照相同的步骤操作,但这次要创建一个使用以下详细信息的 SMS 触发器:

字段
名称 trigger-sms-service
描述 推送到短信分支
触发类型 Branch
分支(正则表达式) ^([sS]ms)/\w+
Cloudbuild 配置 sms-service/cloudbuild.yaml
替代变量
  1. 在屏幕底部选择“保存”按钮以启用触发器。

创建了三个触发器后,Cloud Build 页面应类似于以下截图:

这些触发器各自独立运行,使用不同的分支来标识在特定服务上执行的工作。通过我们的正则表达式过滤器,每个分支将在代码提交到仓库时进行检查,并创建一个新的构建任务。为了使这个过程有效,我们需要将cloudbuild.yaml文件添加到电子邮件和短信目录中,以启动构建过程。

让我们使用cloudbuild.yaml创建一些配置:

  1. email-service目录中,创建一个新的cloudbuild.yaml文件,内容如下:
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/email-service', 'email-service/.']
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/email-service']
# Deploy the image to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args: ['beta', 'run', 'deploy', 'email-service', '--image', 'gcr.io/$PROJECT_ID/email-service', '--region', 'us-central1', '--platform', 'managed', '--no-allow-unauthenticated']
images:
- 'gcr.io/$PROJECT_ID/email-service'
timeout: "600s"

如果你不记得如何在 Git 中执行这些操作,可以回顾早期的示例,了解如何使用 Google 源代码仓库。命令类似,我们只是在不同的分支上工作。

  1. git中创建一个名为email/fix-1的新分支。

  2. 使用git将更新后的cloudbuild.yaml文件添加到email/fix-1分支。

  3. 添加提交信息:初始修订 - email/fix-1

  4. email/fix-1分支的代码推送到仓库。

  5. sms-service目录中,创建一个新的cloudbuild.yaml文件,内容如下:

 steps:
 # Build the container image
 - name: 'gcr.io/cloud-builders/docker'
 args: ['build', '-t', 'gcr.io/$PROJECT_ID/sms-service', 'sms-service/.']
 # Push the container image to Container Registry
 - name: 'gcr.io/cloud-builders/docker'
 args: ['push', 'gcr.io/$PROJECT_ID/sms-service']
 # Deploy the image to Cloud Run
 - name: 'gcr.io/cloud-builders/gcloud'
 args: ['beta', 'run', 'deploy', 'sms-service', '--image', 'gcr.io/$PROJECT_ID/sms-service', '--region', 'us-central1', '--platform', 'managed', '--no-allow-unauthenticated']
 images:
 - 'gcr.io/$PROJECT_ID/sms-service'
 timeout: "600s"
  1. git中创建一个名为sms/fix-1的新分支。

  2. 使用git将更新后的cloudbuild.yaml文件添加到sms/fix-1分支。

  3. 添加提交信息:初始修订 - sms/fix-1

  4. sms/fix-1分支的代码推送到仓库。

  5. 推送最终代码自动启动 Cloud Build 及相关的三阶段脚本。

恭喜!使用 Cloud Build 进行构建过程自动化能够提高开发人员的生产力。现在我们已经集成了基于组件的构建能力,接下来我们可以继续研究如何测试该解决方案的多个组件。

测试实验室服务

从技术上讲,如果需要记录信息,正确的答案通常是 Stackdriver,因为它已经集成到 Google Cloud 中。但是,有时可能需要替代解决方案——例如在这种情况下,需要一种快速简便的方法来检查服务。

在本测试部分,我们不会使用日志记录将数据发送到 Stackdriver 等集中式系统,而是将数据发布到 Google Sheet。像这样的方案非常实用,因此值得将其整合到我们的解决方案中,以展示如何实现这一目标。

从高层次来看,以下流程适用于测试服务:

  1. 向表格写入信息

  2. 应用审核

  3. 漏洞修复

查看下面的图示,以便理解本解决方案中涉及的组件:

在前面的图示中,我们向现有代码中添加了一个新的服务组件,将信息记录到 Google 电子表格中。能够展示解决方案的韧性是构建概念验证的重要功能。电子表格将由服务完成;因此,如果工作正常,输出将在电子表格中显示。如果由于某种原因服务不可用,则会为无法完成任务的服务写入替代状态消息:

电子表格与每个服务动态关联,并在服务调用时将消息写入电子表格单元格。这个新组件将提供一种简单的方法来单独测试每个服务,并显示服务组件是否按规格运行。现在我们已经有了服务和属性的描述,可以开始开发代码库来满足需求。

访问凭据

在这一部分,我们将从访问 Compute Engine 服务账户开始:

  1. 列出与项目关联的服务账户:
 gcloud iam service-accounts list
  1. 复制列出的 Compute Engine 服务账户电子邮件地址。

  2. 打开一个新的 Google Sheets 文档。

  3. 将表格重命名为 实验结果状态

  4. 点击共享按钮,并以完全编辑权限添加 Compute Engine 服务账户电子邮件。

接下来,我们需要设置电子邮件 Cloud Pub/Sub 订阅。

设置电子邮件 Cloud Pub/Sub 订阅

要设置电子邮件 Cloud Pub/Sub 订阅,请按照以下简单步骤操作:

  1. 创建一个具有调用权限的服务账户:
gcloud iam service-accounts create pubsub-cloud-run-invoker --display-name "PubSub Cloud Run Invoker"
  1. 为服务账户授予调用电子邮件服务的权限:
gcloud beta run services add-iam-policy-binding email-service --member=serviceAccount:pubsub-cloud-run-invoker@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com --role=roles/run.invoker --region us-central1 --platform managed
  1. 获取 Google Cloud 项目编号:
PROJECT_NUMBER=$(gcloud projects list --format="value(projectNumber)" --filter="projectId=$GOOGLE_CLOUD_PROJECT")
  1. 使用 PROJECT_NUMBER 环境变量创建 serviceAccountTokenCreator 的角色绑定:
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT --member=serviceAccount:service-$PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com --role=roles/iam.serviceAccountTokenCreator
  1. 获取电子邮件服务的 EMAIL_URL 端点:
 EMAIL_URL=$(gcloud beta run services describe email-service --platform managed --region us-central1 --format "value(status.address.url)")
  1. 创建 Cloud Pub/Sub 订阅:
 gcloud pubsub subscriptions create email-service-sub --topic new-lab-report --push-endpoint=$EMAIL_URL --push-auth-service-account=pubsub-cloud-run-invoker@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com

做得好!我们现在已经为 Cloud Pub/Sub 设置了电子邮件订阅。接下来,我们需要对 SMS 订阅重复这一过程。

设置 SMS Cloud Pub/Sub 订阅

要设置 SMS Cloud Pub/Sub 订阅,请完成以下步骤:

  1. 创建 run.invoker 的角色绑定:
gcloud beta run services add-iam-policy-binding sms-service --member=serviceAccount:pubsub-cloud-run-invoker@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com --role=roles/run.invoker --region us-central1 --platform managed
  1. 获取 sms-serviceSMS_URL 端点:
SMS_URL=$(gcloud beta run services describe sms-service --platform managed --region us-central1 --format "value(status.address.url)")
  1. 创建 Cloud Pub/Sub 订阅:
gcloud pubsub subscriptions create sms-service-sub --topic new-lab-report --push-endpoint=$SMS_URL --push-auth-service-account=pubsub-cloud-run-invoker@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com

太棒了!我们现在已设置好 Cloud Pub/Sub 的两个订阅。接下来,我们需要测试我们的服务。

测试服务

现在,是时候测试服务了。为此,请按照以下步骤操作:

  1. 获取 lab-serviceLAB_URL 端点:
 LAB_URL=$(gcloud beta run services describe lab-service --platform managed --region us-central1 --format "value(status.address.url)")
  1. 向实验报告服务发送一些数据:
curl -X POST \
-H "Content-Type: application/json" \
-d "{\"id\": 12}" \
$LAB_URL &
curl -X POST \
-H "Content-Type: application/json" \
-d "{\"id\": 34}" \
$LAB_URL &
curl -X POST \
-H "Content-Type: application/json" \
-d "{\"id\": 56}" \
$LAB_URL &

在下图中,您可以看到向实验服务发送信息的结果:

恭喜!应用程序现在可以响应外部刺激。

为了测试我们的服务的韧性,我们需要添加一个错误,使得该服务无法处理信息。对于这个示例,我们将使用邮件服务来引入一个错误。

  1. 切换到 ch12/email-service 子目录并编辑 index.js 源代码——添加错误的 oops 行条目:
Add an error in the app.post function
...
app.post('/', async (req, res) => {
oops
const labReport = util.decodeBase64Json(req.body.message.data);
...
  1. 输入无效代码将确保服务在调用时无法成功完成。

  2. 提交更新后的 email-service 代码。

  3. 代码将自动构建、推送到注册表并部署。

现在,当应用程序运行时,邮件服务将无法完成,因为错误的行条目 oops 阻止了服务的正常工作。因此,电子表格中将不会写入任何条目。要验证这一点,请检查 Stackdriver 日志,查看是否显示与输入的缺陷相关的错误。

最后,修正源文件,删除代码缺陷,以恢复正常服务并确保信息成功记录到电子表格中。

恭喜!我们现在已经成功完成了各个组件的测试。接下来,我们将概述本章的内容。

总结

在本章的最后,我们构建了一个应用程序来基于 JSON 消费外部信息。值得一提的是,我们使用了一个可扩展且具有弹性的机制来处理消息传递,以便无缝地进行重试。我们还学习了如何结合使用 Google Sheets 来输出信息。像这样的新技术有许多应用,且将一再证明其有用。为了完善我们的应用程序,我们还关注了开发人员的生产力,以便在构建多个组件时可以使用像 Cloud Build 这样的工具。

在本章中,你已经学习了如何与 Google API 集成,启动 Cloud Pub/Sub,以及如何从外部服务获取 JSON 信息。学习如何聚合服务是使用 Google Cloud 无服务器架构的一部分。例如,使用 Cloud Pub/Sub 实现可扩展的应用程序消息传递是非常简单的。前几章中使用的每一个模式都可以轻松地扩展到你的项目中。希望你能够创造下一个大事件,或鼓励你的同事在 Google Cloud 上构建更棒的产品。

问题

  1. Google Cloud 上可以用于异步消息传递的产品有哪些?

  2. 你能列举一些 Google Cloud Storage 支持的通知吗?

  3. 为什么 GCloud SDK 中会有测试版命令?

  4. 什么是正则表达式(regex),它为什么对创建分支名称触发器有用?

  5. Cloud Source Repositories 是基于项目的。(对或错)

  6. 如果我想对消息启用速率限制,我会使用 Cloud Tasks 而不是 Cloud Pub/Sub。(对或错)

  7. 我在哪里可以看到与 Cloud Run 应用程序相关的 HTTP 延迟?

进一步阅读

第十三章:评估

第一章:介绍应用引擎

  1. Push Queue 和 Pull Queue 是任务队列支持的服务分发类型。

  2. Memcache 支持的两级服务是共享 Memcache 和专用 Memcache。

  3. Cloud Datastore 是一个托管的无模式(NoSQL)文档数据库。

  4. GAE 支持的语言:Python、Go、Node 和 PHP。

  5. GAE 支持的流量分割算法包括 IP 流量、Cookie 和随机。

  6. GFE 在 GAE 中的作用是通过自动扩展基础设施路由 Google 应用引擎流量。

  7. GAE 支持的三种扩展方式分别是自动扩展、基本扩展和手动扩展。

  8. 任务队列机制用于将长期运行的工作负载与 HTTP 请求/响应生命周期分离,以提高效率。

第二章:使用应用引擎开发

  1. 错误。

  2. GAE 提供的选项包括 IP 地址、Cookie 或随机选项。

  3. 基本和高级是 Stackdriver Logging 中的日志过滤选项。

  4. 错误。

  5. 正确。

  6. gcloud app deploy 命令。

  7. appspot.com 域名。

第三章:介绍轻量级函数

  1. 正确。

  2. 延迟或推迟的任务是异步任务。

  3. 正确。

  4. 正确。

  5. trigger-http 命令。

  6. Stackdriver Logging。

  7. gcloud functions deploy NAME --runtime RUNTIME TRIGGER 命令。

  8. exports 函数是入口点——这表示 Cloud Functions 接口是公开的。

第四章:开发云函数

  1. Cloud Functions 运行在 8080 端口。

  2. 主题触发器由 Cloud Pub/Sub 使用。

  3. Go、Node 和 Python 是 Google Cloud Functions 支持的运行时语言。

  4. 成功的 HTTP 响应代码是 2xx。

  5. 客户端错误的 HTTP 响应代码是 4xx。

  6. 服务器端错误的 HTTP 响应代码是 5xx。

  7. CORS 的目的是允许从另一个域访问网页上的资源。

  8. HTTP 头 – 在头部名称中添加 access-control-allow-origin

第五章:探索作为服务的函数

  1. Cloud Pub/Sub 支持推送/拉取订阅。

  2. 直通处理,多发布者,多订阅者。

  3. GET、DELETE、POST 和 PUT 动词与 HTTP 相关。

  4. 使用 GET 使用户数据可以在 URL 中访问。

  5. 元数据属性包含与 Cloud Storage 对象相关的信息。

  6. 服务器上的问题。

  7. 是的,Google Cloud 支持 OAuth v2。

  8. 不——消息将排队等待重试。

第六章:云函数实验室

  1. 调用 Cloud Function 需要 cloudfunctions.invoker 权限。

  2. Google KMS 提供加密密钥的存储。

  3. allUsers 权限实际上意味着公开访问适用于所有人。

  4. allAuthenticated 权限实际上意味着公开访问适用于经过身份验证的用户。

  5. --max-instances 参数。

  6. gcloud functions add-iam-policy 命令。

  7. gcloud iam service-accounts create 命令。

  8. Bastion 主机是一个隔离的主机,用于限制网络的进出访问。

第七章:介绍 Cloud Run

  1. 单体应用与微服务应用的区别在于,微服务是松耦合架构、隔离的、标准接口、更易调试。而单体应用通常是紧耦合的,调试困难。

  2. GFE 执行将流量路由到项目资源的功能。

  3. 两种同步事件处理模式:请求/响应、Sidewinder。

  4. ENTRYPOINT 关键字用于在容器执行时运行命令。

  5. docker build -t NAME . 命令。

  6. 容器注册表是 Google Cloud 用于镜像管理的产品。

  7. 自动化。

  8. Knative(中间件)位于应用程序与 K8s 之间。

  9. 开放容器倡议(OCI)——标准化的容器接口。

  10. Debian、Ubuntu 和 Windows 是支持容器的一些操作系统。

第八章:使用 Cloud Run 开发

  1. 基础 URL 是访问服务的端点。

  2. API 版本控制非常重要,因为它支持多个版本的部署。

  3. GET、PUT、POST 和 DELETE 方法预计将在 REST API 中可用。

  4. 当客户端发送的数据不正确时,返回 HTTP 状态码 4xx 是合适的。

  5. Cloud Build 的最大持续时间为 600 秒。

  6. 正确。

  7. Cloud Build 错误将在 Cloud Build 仪表板(概览)和历史记录(详细视图)中显示。

第九章:使用 Cloud Run for Anthos 开发

  1. 在使用 Cloud Run for Anthos 时,支持的机器定制类型包括操作系统、GPU、CPU。

  2. 使用 Cloud Run for Anthos 时,SSL 证书是手动配置的。

  3. 部署到 Cloud Run for Anthos 时,--platform gke 平台标志是必需的。

  4. 使用 Cloud Run for Anthos 时,端口 8080 用于服务访问。

  5. 正确。

  6. Cloud Run for Anthos 是使用 Cloud Run 所必需的。

  7. kubectl 命令。

  8. Pod 是 Kubernetes 中的副本单位。

  9. GKE 支持通过 Ingress 处理外部流量。

第十章:Cloud Run 实验室

  1. 使用 Container Registry 而非其他注册表的一个关键优势是 Container Registry 位于 Google 网络内。

  2. 在命令行脚本中使用如 $GOOGLE_CLOUD_PROJECT 这样的环境变量非常有用。

  3. Cloud Build 支持分支和标签类型。

  4. @cloudbuild.gserviceaccount.com 命令。

  5. Dockerfile 和 Cloudbuild.yaml 得到 Cloud Build 的支持。

  6. Let's Encrypt 提供 Certbot SSL 证书。

第十一章:构建 PDF 转换服务

  1. gsutil 命令。

  2. Stackdriver Logging 在与日志相关的问题出现时最为有用。

  3. Docker 清单文件的目的是为要构建的镜像提供规范。

  4. Cloud Build 在创建时将镜像存储在容器注册表中。

  5. 正确。

  6. 使用带有 -m 参数的 gsutil 命令进行 Cloud Storage 的多区域请求。

  7. 调用服务所需的权限类型是 invoker

第十二章:通过 REST API 消费第三方数据

  1. Google Cloud 用于异步消息传递的产品是 Cloud Pub/Sub。

  2. Google Cloud Storage 支持的通知包括 finalize、delete、archive 和 metadataUpdate。

  3. 对于新服务/产品的测试,GCloud SDK 提供了 beta 命令。

  4. 正则表达式(Regex)是用于指定文本的常规表达式/简写形式。

  5. 正确。

  6. 正确。

  7. Stackdriver Trace。

posted @ 2025-07-08 12:24  绝不原创的飞龙  阅读(74)  评论(0)    收藏  举报