Python-云原生教程-全-

Python 云原生教程(全)

原文:zh.annas-archive.org/md5/7CEC2A066F3DD2FF52013764748D267D

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

如今的企业发展如此迅速,以至于拥有自己的基础架构来支持扩张已经不可行。因此,它们一直在利用云的弹性来提供一个构建和部署高度可扩展应用的平台。

这本书将是你学习如何在 Python 中构建云原生架构的一站式书籍。它将首先向你介绍云原生架构,并帮助你分解它。然后你将学习如何使用事件驱动方法在 Python 中构建微服务,并构建 Web 层。接下来,你将学习如何与数据服务交互,并使用 React 构建 Web 视图,之后我们将详细了解应用安全性和性能。然后,你还将学习如何将你的服务 Docker 化。最后,你将学习如何在 AWS 和 Azure 平台上部署应用。我们将以讨论一些关于部署后应用可能出现的问题的概念和技术来结束本书。

这本书将教会你如何构建作为小型标准单元的应用,使用所有经过验证的最佳实践,并避免通常的陷阱。这是一本实用的书;我们将使用 Python 3 及其令人惊叹的工具生态系统来构建一切。本书将带你踏上一段旅程,其目的地是基于云平台的微服务构建完整的 Python 应用程序。

本书涵盖的内容

第一章《介绍云原生架构和微服务》讨论了基本的云原生架构,并让你准备好构建应用程序。

第二章《使用 Python 构建微服务》为你提供了构建微服务和根据你的用例扩展它们的完整知识。

第三章《使用 Python 构建 Web 应用程序》构建了一个与微服务集成的初始 Web 应用程序。

第四章《与数据服务交互》让你亲自了解如何将你的应用迁移到不同的数据库服务。

第五章《使用 React 构建 Web 视图》讨论了如何使用 React 构建用户界面。

第六章《使用 Flux 创建可扩展的 UI》让你了解了用于扩展应用的 Flux。

第七章《学习事件溯源和 CQRS》讨论了如何以事件形式存储交易以提高应用性能。

第八章《保护 Web 应用程序》帮助你保护应用程序免受外部威胁。

第九章《持续交付》让你了解频繁应用发布的知识。

第十章《将你的服务 Docker 化》讨论了容器服务和在 Docker 中运行应用程序。

第十一章《在 AWS 平台上部署》教会你如何在 AWS 上为你的应用构建基础架构并设置生产环境。

第十二章《在 Azure 平台上实施》讨论了如何在 Azure 上为你的应用构建基础架构并设置生产环境。

第十三章《监控云应用》让你了解不同的基础架构和应用监控工具。

你需要为这本书做好什么准备

您需要在系统上安装 Python。最好使用文本编辑器 Vim/Sublime/Notepad++。在其中一章中,您可能需要下载 POSTMAN,这是一个强大的 API 测试套件,可作为 Chrome 扩展程序使用。您可以在chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en下载。

除了这些之外,如果您在以下网络应用程序上有账户,那将是很好的:

  • Jenkins

  • Docker

  • 亚马逊网络服务

  • Terraform

如果您没有账户,这本书将指导您,或者至少指导您如何在先前提到的网络应用程序上创建账户。

这本书是为谁写的

这本书适用于具有 Python 基础知识、命令行和基于 HTTP 的应用程序原理的开发人员。对于那些想要学习如何构建、测试和扩展他们的基于 Python 的应用程序的人来说,这本书是理想的选择。不需要有 Python 编写微服务的先前经验。

约定

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

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:“创建一个signup路由,该路由将使用GETPOST方法读取页面,并将数据提交到后端数据库。”

代码块设置如下:

    sendTweet(event){
      event.preventDefault();
      this.props.sendTweet(this.refs.tweetTextArea.value); 
      this.refs.tweetTextArea.value = '';
    } 

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

$ apt-get install nodejs

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“单击“创建用户”按钮,用户将被创建,并且策略将附加到其中。”

警告或重要说明会以这种方式出现。

技巧和窍门会以这种方式出现。

第一章:介绍云原生架构和微服务

我们开始吧!在我们开始构建应用程序之前,我们需要找到以下一些问题的答案:

  • 什么是云计算?它有哪些不同类型?

  • 什么是微服务及其概念?

  • 什么是好去做的基本要求?

在本章中,我们将专注于开发人员或应用程序员在开始编写应用程序之前应该了解的不同概念。

让我们先了解一下系统构建及其演变。

长期以来,我们一直在寻找构建系统的新方法。随着新技术的进步和更好方法的采用,IT 基础设施变得更加可靠和有效,为客户提供更好的体验,也让工程师感到满意。

持续交付帮助我们将软件开发周期转移到生产环境,并让我们识别软件中不同易出错的方面,坚持认为每次代码提交都是适合发布到生产环境的候选项。

我们对互联网运作方式的理解已经推动我们发展出更好的让机器与其他机器交流的方法。虚拟化平台使我们能够独立创建解决方案并调整我们的机器大小,基础设施自动化使我们能够以规模管理这些机器。一些大型、成功的云平台,如亚马逊、Azure 和谷歌,已经采纳了小团队拥有其服务的全生命周期的观点。领域驱动设计(DDD)、持续交付(CD)、按需虚拟化、基础设施自动化、小型自治团队和规模化系统等概念是不同特征,它们有效地将我们的软件投入生产。现在,微服务已经从这个世界中崛起。它并不是在现实之前开发或描述的;它是作为一种趋势或者说是从真实使用中崛起的。在本书中,我将从这些先前的工作中提取出一些内容,以帮助说明如何构建、管理和优化微服务。

许多组织发现,通过采用细粒度的微服务结构,他们可以快速交付软件,并掌握更新的技术。微服务基本上给了我们更多的灵活性来做出反应和做出各种决策,允许我们迅速应对不可避免的影响我们所有人的变化。

云计算简介

在我们开始微服务和云原生概念之前,让我们先了解一下云计算的基本概念。

云计算是一个描述广泛的服务的广泛术语。与技术中的其他重大发展一样,许多供应商都抓住了“云”这个词,并将其用于超出基本定义范围的产品。由于云是一个广泛的服务集合,组织可以选择何时、何地以及如何使用云计算。

云计算服务可以分为以下几类:

  • SaaS:这些是准备好被最终用户接受的成熟应用程序

  • PaaS:这些是一组对于想要构建他们的应用程序或快速将其直接托管到生产环境而不必关心底层硬件的用户/开发人员有用的工具和服务

  • IaaS:这是为想要构建自己的业务模型并自定义它的客户提供的服务

云计算作为一个堆栈,可以解释如下:

  • 云计算通常被称为堆栈,基本上是一系列服务,其中每个服务都建立在另一个服务的基础上,统称为“云”。

  • 云计算模型被认为是一组不同的可配置计算资源(如服务器、数据库和存储),它们彼此通信,并且可以在最少监督下进行配置。

以下图表展示了云计算堆栈组件:

让我们详细了解云计算组件及其用例。

软件即服务

以下是描述 SaaS 的关键要点:

  • 软件即服务(SaaS)为用户提供了访问托管在服务提供商场所的软件的能力,通过提供商通过互联网提供的服务作为服务通过 Web 浏览器。这些服务基于订阅,并且也被称为按需软件。

  • SaaS 提供公司包括谷歌文档生产套件、甲骨文 CRM(客户关系管理)、微软及其 Office 365 提供和 Salesforce CRM 和 QuickBooks。

  • SaaS 还可以进一步分类为专业 SaaS,专注于特定行业的需求,如医疗保健和农业,或横向 SaaS,专注于软件行业,如人力资源和销售。

  • SaaS 提供基本上是为那些迅速想要掌握现有应用程序的组织而设计的,这些应用程序易于使用和理解,即使对于非技术人员也是如此。根据组织的使用和预算,企业可以选择支持计划。此外,您可以从全球任何地方的任何设备上访问这些 SaaS 应用程序,并且具有互联网功能。

平台即服务

以下是描述 PaaS 的关键要点:

  • 在 PaaS 提供中,组织/企业无需担心其内部应用程序的硬件和软件基础设施管理

  • PaaS 的最大好处是为开发团队(本地或远程),他们可以在一个共同的框架上高效地构建、测试和部署他们的应用程序,其中底层硬件和软件由 PaaS 服务提供商管理。

  • PaaS 服务提供商提供平台,并在平台周围提供不同的服务

  • PaaS 提供商的示例包括亚马逊网络服务(AWS Elastic Beanstalk)、微软 Azure(Azure 网站)、谷歌应用引擎和甲骨文(大数据云服务)

基础设施即服务

以下是描述 IaaS 的关键要点:

  • 与 SaaS 不同,在 IaaS 中,客户提供 IT 资源,例如裸金属机器来运行应用程序,硬盘用于存储,以及网络电缆用于网络功能,他们可以根据其业务模型进行定制。

  • 在 IaaS 提供中,由于客户可以完全访问其基础设施,他们可以根据其应用程序的要求扩展其 IT 资源。此外,在 IaaS 提供中,客户必须管理应用程序/资源的安全性,并需要在突发故障/崩溃时建立灾难恢复模型。

  • 在 IaaS 中,服务是按需提供的,客户根据使用情况收费。因此,客户有责任对其资源进行成本分析,这将有助于限制他们超出预算。

  • 它允许客户/消费者根据应用程序的要求定制其基础设施,然后快速高效地拆除基础设施并重新创建。

  • 基于 IaaS 的定价模型基本上是按需提供的,这意味着您按需付费。您根据资源的使用和使用时间收费。

  • 亚马逊网络服务(提供 Amazon Elastic Compute Cloud(Amazon EC2)和 Amazon Simple Storage Service(Amazon S3))是云服务中的第一个,然而,微软 Azure(虚拟机)、Rackspace(虚拟云服务器)和甲骨文(裸金属云服务)等公司也声名显赫。

云原生概念

云原生是构建团队、文化和技术,利用自动化和架构来管理复杂性并释放速度。

云原生概念超越了与其相关的技术。我们需要了解公司、团队和个人是如何取得成功的,才能了解我们的行业将走向何方。

目前,像 Facebook 和 Netflix 这样的公司已经投入了大量资源来研究云原生技术。即使是小型和更灵活的公司现在也意识到了这些技术的价值。

通过云原生的成熟实践的反馈,以下是一些显而易见的优势:

  • 以结果为导向和团队满意度:云原生方法展示了将一个大问题分解成小问题的方式,这样每个团队可以专注于个别部分。

  • 繁重的工作:自动化减少了引起运营痛苦的重复手动任务,并减少了停机时间。这使得您的系统更加高效,并且产生更加高效的结果。

  • 可靠高效的应用程序基础设施:自动化使得在不同环境中部署更加可控——无论是开发、阶段还是生产环境——并且还可以处理意外事件或故障。构建自动化不仅有助于正常部署,而且在灾难恢复情况下也使部署变得更加容易。

  • 对应用程序的洞察:围绕云原生应用程序构建的工具提供了更多对应用程序的洞察,使它们易于调试、故障排除和审计。

  • 高效可靠的安全性:在每个应用程序中,主要关注点是其安全性,并确保它可以通过所需的渠道进行身份验证。云原生方法为开发人员提供了确保应用程序安全性的不同方式。

  • 成本效益的系统:云方法管理和部署您的应用程序使资源的使用更加高效,这也包括应用程序发布,因此通过减少资源的浪费使系统更加具有成本效益。

云原生——它的含义和重要性是什么?

云原生是一个广义术语,利用不同的技术,如基础设施自动化、开发中间件和支持服务,这些基本上是您的应用程序交付周期的一部分。云原生方法包括频繁的无故障和稳定的软件发布,并且可以根据业务需求扩展应用程序。

使用云原生方法,您将能够以系统化的方式实现应用程序构建的目标。

云原生方法远比传统的虚拟化导向编排更好,后者需要大量工作来构建适合开发的环境,然后为软件交付过程构建一个完全不同的环境。理想的云原生架构应该具有自动化和组合功能,可以代表您工作。这些自动化技术还应该能够管理和部署您的应用程序到不同的平台,并为您提供结果。

您的云原生架构还应该能够识别一些其他运营因素,如稳定的日志记录、监控应用程序和基础设施,以确保应用程序正常运行。

云原生方法确实帮助开发人员使用诸如 Docker 之类的工具在不同平台上构建其应用程序,Docker 是轻量级且易于创建和销毁的。

云原生运行时

容器是如何在从一个计算环境移动到另一个计算环境时可靠运行软件的最佳解决方案。这可能是从一个开发者机器到阶段环境,再到生产环境,也可能是从物理机器到私有或公共云中的虚拟机。Kubernetes 已经成为容器服务的代名词,并且正在变得越来越流行。

随着云原生框架的兴起和围绕其构建的应用程序数量的增加,容器编排的属性受到了更多的关注和使用。以下是您从容器运行时需要的内容:

  • 管理容器状态和高可用性:务必在生产环境中维护容器的状态(如创建和销毁),因为这对于业务至关重要,并且应能够根据业务需求进行扩展

  • 成本分析和实现:容器可以根据您的业务预算控制资源管理,并且可以大大降低成本

  • 隔离环境:在容器内运行的每个进程应保持在该容器内部隔离

  • 跨集群负载平衡:应用程序流量基本上由一组容器集群处理,应在容器内平衡重定向,这将增加应用程序的响应并保持高可用性

  • 调试和灾难恢复:由于我们在处理生产系统,因此需要确保我们拥有正确的工具来监视应用程序的健康状况,并采取必要的措施以避免停机并提供高可用性

云原生架构

云原生架构与我们为传统系统创建的任何应用程序架构类似,但在云原生应用程序架构中,我们应该考虑一些特征,例如十二要素应用程序(应用程序开发的模式集合)、微服务(将单块业务系统分解为独立的可部署服务)、自助式敏捷基础设施(自助式平台)、基于 API 的协作(通过 API 进行服务之间的交互)和反脆弱性(自我实现和加强应用程序)。

首先,让我们讨论一下什么是微服务?

微服务是一个更广泛的术语,将大型应用程序分解为较小的模块进行开发,并使其足够成熟以发布。这种方法不仅有助于有效管理每个模块,还可以在较低级别识别问题。以下是微服务的一些关键方面:

  • 用户友好的界面:微服务使微服务之间能够清晰分离。微服务的版本控制使 API 更易于控制,并且还为这些服务的消费者和生产者提供了更多自由。

  • 在平台上部署和管理 API:由于每个微服务都是一个独立的实体,因此可以更新单个微服务而无需更改其他微服务。此外,对于微服务来说,回滚更容易。这意味着部署的微服务的构件在 API 和数据模式方面应兼容。这些 API 必须在不同平台上进行测试,并且测试结果应该在不同团队之间共享,即运营、开发人员等,以维护一个集中的控制系统。

  • 应用程序的灵活性:开发的微服务应能够处理请求并必须做出响应,无论请求的类型如何,可能是错误的输入或无效的请求。此外,您的微服务应能够处理意外的负载请求并做出适当的响应。所有这些微服务都应该独立测试,以及进行集成测试。

  • 微服务的分发:最好将服务分割成小块服务,以便可以单独跟踪和开发,并组合成一个微服务。这种技术使微服务的开发更加高效和稳定。

以下图表显示了云原生应用程序的高级架构:

应用程序架构理想上应该从两个或三个服务开始,尝试通过进一步的版本扩展。了解应用程序架构非常重要,因为它可能需要与系统的不同组件集成,并且在大型组织中,可能有一个单独的团队管理这些组件。在微服务中进行版本控制非常重要,因为它标识了在开发的指定阶段支持的方法。

微服务是一个新概念吗?

微服务在行业中已经存在很长时间了。这是创建大型系统的不同组件之间的区别的另一种方式。微服务以类似的方式工作,它们作为不同服务之间的链接,并根据请求类型处理特定交易的数据流。

以下图表描述了微服务的架构:

为什么 Python 是云原生微服务开发的最佳选择?

为什么我选择 Python,并推荐尽可能多的人使用它?嗯,这归结于下面部分中解释的原因。

可读性

Python 是一种高度表达性和易于学习的编程语言。即使是业余爱好者也可以轻松发现 Python 的不同功能和范围。与其他编程语言(如 Java)不同,它更注重括号、括号、逗号和冒号,Python 让你花更多时间在编程上,而不是在调试语法上。

库和社区

Python 的广泛库范围在不同平台(如 Unix、Windows 或 OS X)上非常便携。这些库可以根据您的应用程序/程序要求轻松扩展。有一个庞大的社区致力于构建这些库,这使得它成为商业用例的最佳选择。

就 Python 社区而言,Python 用户组PUG)是一个致力于通过基于社区的开发模型增加 Python 在全球范围内的知名度的社区。这些团体成员就基于 Python 的框架发表演讲,这有助于我们构建大型系统。

交互模式

Python 交互模式可帮助您调试和测试代码片段,稍后可以将其作为主程序的一部分添加。

可扩展性

Python 提供了更好的结构和概念,例如模块,以比任何其他脚本语言(如 shell 脚本)更系统地维护大型程序。

了解十二要素应用程序

云原生应用程序符合旨在通过可预测的实践增强灵活性的协议。这个应用程序保持了一种名为十二要素应用程序的宣言。它概述了开发人员在构建现代基于 Web 的应用程序时应遵循的方法论。开发人员必须改变他们的编码方式,为他们的应用程序运行的基础设施之间创建一个新的合同。

在开发云原生应用程序时,有几点需要考虑:

  • 使用信息化设计,通过自动化增加应用程序的使用率,减少客户的时间和成本

  • 在不同环境(如阶段和生产)和不同平台(如 Unix 或 Windows)之间使用应用程序可移植性

  • 使用云平台上的应用程序适用性,并了解资源分配和管理

  • 使用相同的环境来减少 bug,并通过持续交付/部署实现软件发布的最大灵活性

  • 通过最小的监督扩展应用程序并设计灾难恢复架构,实现高可用性

许多十二要素相互作用。它们通过强调声明性配置,专注于速度、安全性和规模。十二要素应用程序可以描述如下:

  • 集中式代码库:每个部署的代码都在修订控制中进行跟踪,并且应该在多个平台上部署多个实例。

  • 依赖管理:应用程序应能够声明依赖关系,并使用诸如 Bundler、pip 和 Maven 等工具对其进行隔离。

  • 定义配置:在操作系统级别定义可能在不同部署环境(如开发、阶段和生产)中不同的配置(即环境变量)。

  • 后备服务:每个资源都被视为应用程序本身的一部分。后备服务,如数据库和消息队列,应被视为附加资源,并在所有环境中平等消耗。

  • 在构建、发布和运行周期中的隔离:这涉及在构建工件之间进行严格分离,然后与配置结合,然后从工件和配置组合中启动一个或多个实例。

  • 无状态进程:应用程序应执行一个或多个实例/进程(例如,主/工作者),它们之间没有共享。

  • 服务端口绑定:应用程序应是自包含的,如果需要暴露任何/所有服务,则应通过端口绑定(最好是 HTTP)来实现。

  • 扩展无状态进程:架构应强调在底层平台中管理无状态进程,而不是向应用程序实现更多复杂性。

  • 进程状态管理:进程应该能够快速扩展并在短时间内优雅地关闭。这些方面可以实现快速扩展性、部署更改和灾难恢复。

  • 持续交付/部署到生产环境:始终尝试保持不同环境的相似性,无论是开发、阶段还是生产。这将确保您在多个环境中获得类似的结果,并实现从开发到生产的持续交付。

  • 日志作为事件流:日志记录非常重要,无论是平台级还是应用程序级,因为这有助于了解应用程序的活动。启用不同的可部署环境(最好是生产环境)通过集中服务收集、聚合、索引和分析事件。

  • 临时任务作为一次性进程:在云原生方法中,作为发布的一部分运行的管理任务(例如数据库迁移)应作为一次性进程运行到环境中,而不是作为具有长时间运行进程的常规应用程序。

云应用平台,如 Cloud Foundry、Heroku 和 Amazon Beanstalk,都经过优化,用于部署十二要素应用。

考虑所有这些标准,并将应用程序与稳定的工程接口集成,即处理无状态概要设计,使得分布式应用程序具备云准备能力。Python 通过其固执、传统而非设置的方式,彻底改变了应用程序系统的发展。

设置 Python 环境

正如我们将在本书中展示的那样,拥有正确的环境(本地或用于自动化构建)对于任何开发项目的成功至关重要。如果工作站具有正确的工具,并且设置正确,那么在该工作站上进行开发会感觉像是一股清新的空气。相反,一个设置不良的环境会让任何开发人员使用起来感到窒息。

以下是我们在本书后期需要的先决条件账户:

  • 需要创建 GitHub 账户进行源代码管理。使用以下链接中的文章来创建:

medium.com/appliedcode/setup-github-account-9a5ec918bcc1

现在,让我们设置一些在开发项目中需要的工具。

安装 Git

Git (git-scm.com) 是一个免费的开源分布式版本控制系统,旨在处理从小型到非常大型的项目,速度和效率都很高。

在基于 Debian 的发行版 Linux(如 Ubuntu)上安装 Git

您可以通过几种方式在 Debian 系统上安装 Git:

  1. 使用高级软件包工具APT)软件包管理工具:

您可以使用 APT 软件包管理工具更新本地软件包索引。然后,您可以以 root 用户身份使用以下命令下载并安装最新的 Git:

      $ apt-get update -y
      $ apt-get install git -y  

上述命令将在您的系统上下载并安装 Git。

  1. 使用源代码,您可以执行以下操作:

  2. 从 GitHub 存储库下载源代码,并从源代码编译软件。

在开始之前,让我们首先安装 Git 的依赖项;以 root 用户身份执行以下命令:

      $ apt-get update -y 
      $ apt-get install build-essential libssl-dev
      libcurl4-gnutls-dev libexpat1-dev gettext unzip -y   

  1. 安装必要的依赖项后,让我们转到 Git 项目存储库(github.com/git/git)下载源代码,如下所示:
      $ wget https://github.com/git/git/archive/v1.9.1.zip -Ogit.zip  

  1. 现在,使用以下命令解压下载的 ZIP 文件:
      $ unzip git.zip
      $ cd git-*  

  1. 现在,您必须制作软件包并以 sudo 用户身份安装它。为此,请使用接下来给出的命令:
      $ make prefix=/usr/local all
      $ make prefix=/usr/local install

上述命令将在您的系统上安装 Git 到/usr/local

在基于 Debian 的发行版上设置 Git

现在我们已经在系统上安装了 Git,我们需要设置一些配置,以便为您生成的提交消息包含您的正确信息。

基本上,我们需要在配置中提供名称和电子邮件。让我们使用以下命令添加这些值:

$ git config --global user.name "Manish Sethi"
$ git config --global user.email manish@sethis.in  

在 Windows 上安装 Git

让我们在 Windows 上安装 Git;您可以从官方网站(git-scm.com/download/win)下载最新版本的 Git。按照下面列出的步骤在 Windows 系统上安装 Git:

  1. 下载.exe文件后,双击运行。首先,您将看到 GNU 许可证,如此截图所示:

点击下一步:

在前面截图中显示的部分中,您可以根据需要自定义设置,或者保持默认设置,这对于本书来说是可以的。

  1. 另外,您可以安装 Git Bash 和 Git;点击下一步:

  1. 在下一个截图中看到的部分中,您可以启用与 Git 软件包一起提供的其他功能。然后,点击下一步:

  1. 您可以通过点击下一步跳过其余步骤,然后进行安装部分。

安装完成后,您将能够看到如下屏幕:

太好了!我们已经成功在 Windows 上安装了 Git!

使用 Chocolatey

这是我在 Windows 10 上安装 Git 的首选方式。它以一行安装与之前相同的软件包。如果您还没有听说过 Chocolatey,请停下一切,去多了解一些。它可以用单个命令安装软件;您不再需要使用点击安装程序!

Chocolatey 非常强大,我将其与Boxstarter结合使用来设置我的开发机器。如果您负责在 Windows 上为开发人员设置机器,这绝对值得一试。

让我们看看您如何使用 Chocolatey 安装 Git。我假设您已经安装了 Chocolatey(chocolatey.org/install)(在命令提示符中是一行)。然后,简单地打开管理员命令窗口,并输入此命令:

$ choco install git -params '"/GitAndUnixToolsOnPath"'  

这将安装 Git 和BASH工具,并将它们添加到您的路径中。

在 Mac 上安装 Git

在开始 Git 安装之前,我们需要为 OS X 安装命令行工具。

为 OS X 安装命令行工具

为了安装任何开发者,您需要安装 Xcode(developer.apple.com/xcode/),这是一个将近 4GB 的开发者套件。苹果公司在 Mac App Store 上免费提供。为了安装 Git 和 GitHub 设置,您需要安装一些命令行工具,这些工具是 Xcode 开发工具的一部分。

如果您有足够的空间,下载并安装 Xcode,这基本上是一个完整的开发工具包。

您需要在developer.apple.com上创建一个苹果开发者帐户,以便下载命令行工具。设置好您的帐户后,您可以根据版本选择命令行工具或 Xcode,如下所示:

  • 如果您使用的是 OS X 10.7.x,下载 10.7 命令行工具。如果您使用的是 OS X 10.8.x,下载 10.8 命令行工具。

  • 下载完成后,打开DMG文件,并按照说明进行安装。

在 OS X 上安装 Git

在 Mac 上安装 Git 与在 Windows 上安装 Git 基本相似。不同之处在于,我们有dmg文件而不是.exe文件,您可以从 Git 网站(https://git-scm.com/download/mac)下载进行安装:

  1. 双击下载的dmg文件。它将打开一个包含以下文件的查找器:

  1. 双击git-2.10.1-intel-universal-mavericks.dmg文件;它将打开安装向导进行安装,如下截图所示:

  1. 点击安装开始安装:

  1. 安装完成后,您将看到类似以下的内容:

如果您使用的是 OS X 10.8,并且尚未修改安全设置以允许安装第三方应用程序,则需要在 OS X 允许您安装这些工具之前进行调整。

安装和配置 Python

现在,让我们安装 Python,我们将使用它来构建我们的微服务。我们将在整本书中使用 Python 3.x 版本。

在基于 Debian 的发行版(如 Ubuntu)上安装 Python

在基于 Debian 的发行版上安装 Python 有不同的方法。

使用 APT 软件包管理工具

您可以使用 APT 软件包管理工具更新本地软件包索引。然后,您可以以 root 用户身份使用以下命令下载并安装最新的 Python:

$ apt-get update -y
$ apt-get install python3 -y  

以下软件包将自动下载并安装,因为这些是 Python 3 安装的先决条件:

libpython3-dev libpython3.4 libpython3.4-dev python3-chardet

python3-colorama python3-dev python3-distlib python3-html5lib

python3-requests python3-six python3-urllib3 python3-wheel python3.4-de

一旦安装了先决条件,它将在您的系统上下载并安装 Python。

使用源代码

您可以从 GitHub 存储库下载源代码并从源代码编译软件,如下所示:

  1. 在开始之前,让我们首先安装 Git 的依赖项;以 root 用户身份执行以下命令来完成:
      $ apt-get update -y 
      $ apt-get install build-essential checkinstall libreadline-gplv2-
         dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-
         dev libc6-dev libbz2-dev -y   

  1. 现在,让我们使用以下命令从 Python 的官方网站下载 Python(www.python.org)。您也可以根据需要下载最新版本:
      $ cd /usr/local
      $ wget https://www.python.org/ftp/python/3.4.6/Python-3.4.6.tgz  

  1. 现在,让我们使用以下命令提取已下载的软件包:
      $ tar xzf Python-3.4.6.tgz  

  1. 现在我们必须编译源代码。使用以下一组命令来完成:
      $ cd python-3.4.6
      $ sudo ./configure
      $ sudo make altinstall  

  1. 上述命令将在/usr/local上安装 Python。使用以下命令检查 Python 版本:
      $ python3 -V 
        Python 3.4.6

在 Windows 上安装 Python

现在,让我们看看如何在 Windows 7 或更高版本系统上安装 Python。在 Windows 上安装 Python 非常简单快捷;我们将使用 Python 3 及以上版本,您可以从 Python 的下载页面(www.python.org/downloads/windows/)下载。现在执行以下步骤:

  1. 根据您的系统配置下载 Windows x86-64 可执行安装程序,并打开它开始安装,如下截图所示:

  1. 接下来,选择要进行的安装类型。我们将点击“立即安装”以进行默认安装,如此截图所示:

  1. 安装完成后,将看到以下屏幕:

太棒了!我们已成功在 Windows 上安装了 Python。

在 Mac 上安装 Python

在开始 Python 安装之前,我们需要安装 OS X 的命令行工具。如果您在安装 Git 时已经安装了命令行工具,可以忽略此步骤。

在 OS X 上安装命令行工具

为了安装任何开发人员,您需要安装 Xcode (developer.apple.com/xcode/);您需要在connect.apple.com上设置一个帐户以下载相应的 Xcode 版本工具。

然而,还有另一种方法可以使用一个实用程序安装命令行工具,该实用程序随 Xcode 一起提供,名为xcode-select,如下所示:

% xcode-select --install  

上述命令应触发命令行工具的安装向导。按照安装向导的指示,您将能够成功安装它。

在 OS X 上安装 Python

在 Mac 上安装 Python 与在 Windows 上安装 Git 非常相似。您可以从官方网站(www.python.org/downloads/)下载 Python 包。按照以下步骤进行:

  1. Python 包下载完成后,双击开始安装;将显示以下弹出窗口:

  1. 接下来的步骤将涉及发布说明和相应的 Python 版本信息:

  1. 接下来,您需要同意许可协议,这是安装的必要步骤:

  1. 接下来,它将显示安装相关信息,如磁盘占用和路径。点击“安装”开始:

  1. 安装完成后,您将看到以下屏幕:

  2. 使用以下命令查看 Python 版本是否已安装:

      % python3 -V  
        Python 3.5.3 

太棒了!Python 已成功安装。

熟悉 GitHub 和 Git 命令

在本节中,我们将介绍一系列我们将在整本书中经常使用的 Git 命令:

  • git init:此命令在首次设置本地存储库时初始化您的本地存储库

  • git remote add origin :此命令将您的本地目录链接到远程服务器存储库,以便所有推送的更改都保存在远程存储库中

  • git status:此命令列出尚未添加或已修改并需要提交的文件/目录

  • git add *或 git add :此命令添加文件/目录,以便可以跟踪它们,并使它们准备好提交

  • git commit -m "Commit message":此命令可帮助您在本地机器上提交跟踪更改,并生成提交 ID,通过该 ID 可以识别更新的代码

  • git commit -am "Commit message":与上一个命令的唯一区别是,此命令在将所有文件添加到暂存区后,会打开默认编辑器,以根据 Ubuntu(Vim)或 Windows(Notepad++)等操作系统添加提交消息。

  • git push origin master:此命令将最后提交的代码从本地目录推送到远程存储库

测试一切,确保我们的环境正常工作。

我们已经在上一节中安装了 Git 和 Python,这些是构建微服务所需的。在本节中,我们将专注于测试已安装的软件包,并尝试熟悉它们。

我们可以做的第一件事是运行 Git 命令,该命令从存储库(通常是 GitHub)上的 HTTPs 获取外部 Python 代码,并将其复制到当前工作空间的适当目录中:

$ git clone https://github.com/PacktPublishing/Cloud-Native-
 Python.git

上述命令将在本地机器上创建一个名为Cloud-Native-Python的目录;从当前位置切换到Cloud-Native-Python/chapter1路径

我们需要安装应用程序的要求,以便运行它。在这种情况下,我们只需要 Flask 模块可用:

$ cd hello.py
$ pip install requirements.txt

在这里,Flask 充当 Web 服务器;我们将在下一章中详细了解它。

安装成功后,您可以使用以下命令运行应用程序:

$ python hello.py
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)  

我认为我们可以看到输出,如下所示:

$ curl http://0.0.0.0:5000/
Hello World!  

如果您看到此输出,则我们的 Python 开发环境已正确设置。

现在是时候编写一些 Python 代码了!

摘要

在本章中,我们开始探索云平台和云计算堆栈。在本章中,您了解了不同的十二要素应用程序方法论,以及它们如何帮助开发微服务。最后,您了解了开发人员应该具备怎样的理想设置环境,以便创建或开始应用程序的创建。

在下一章中,我们将通过创建后端 REST API 并使用 API 调用或 Python 框架进行测试来开始构建我们的微服务。

第二章:使用 Python 构建微服务

现在,既然您了解了微服务是什么,并且希望您对它们的关键优势有所了解,我相信您迫不及待地想要开始构建它们。在本章中,我们将立即开始编写 REST API,这些 API 共同作为微服务工作。

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

  • 构建 REST API

  • 测试 API

Python 概念

让我们首先了解一些 Python 的概念,这些概念将在本书中使用。

模块

模块基本上允许您逻辑地组织您的编程代码。它类似于任何其他 Python 程序。在需要仅导入少量代码而不是整个程序的情况下,我们需要它们。模块可以是一个或多个函数类的组合,以及其他许多内容。我们将使用一些内置函数,它们是 Python 库的一部分。此外,我们将根据需要创建自己的模块。

以下示例代码展示了模块的结构:

    #myprogram.py 
    ### EXAMPLE PYTHON MODULE
    # Define some variables:
    numberone = 1
    age = 78

    # define some functions
    def printhello():
     print "hello"

    def timesfour(input):
     print input * 4

    # define a class
    class house:
     def __init__(self):
         self.type = raw_input("What type of house? ")
         self.height = raw_input("What height (in feet)? ")
         self.price = raw_input("How much did it cost? ")
         self.age = raw_input("How old is it (in years)? ")

     def print_details(self):
         print "This house is a/an " + self.height + " foot",
         print self.type, "house, " + self.age, "years old and costing\
         " + self.price + " dollars." 

您可以使用以下命令导入前面的模块:

# import myprogram

函数

函数是一块组织良好的、自包含的程序块,执行特定任务,您可以将其合并到自己的更大的程序中。它们的定义如下:

    # function 
    def  functionname(): 
      do something 
      return 

以下是需要记住的几点:

  • 缩进在 Python 程序中非常重要

  • 默认情况下,参数具有位置行为,您需要按照它们在定义时的顺序进行通知

请参阅以下代码片段示例,其中展示了函数:

    def display ( name ): 
    #This prints a passed string into this function 
      print ("Hello" + name) 
      return;

您可以按以下方式调用前面的函数:

    display("Manish") 
    display("Mohit") 

以下截图显示了前面的 display 函数的执行情况:

请注意,如果您的系统上安装了多个 Python 版本,您需要使用 Python 3 而不是 Python,后者使用 Python 的默认版本(通常是 2.7.x)。

建模微服务

在本书中,我们将开发一个完整的独立工作的 Web 应用程序。

现在,既然我们对 Python 有了基本的了解,让我们开始对我们的微服务进行建模,并了解应用程序的工作流程。

以下图显示了微服务架构和应用程序工作流程:

构建微服务

在本书中,我们将使用 Flask 作为构建微服务的 Web 框架。Flask 是一个强大的 Web 框架,易于学习和简单易用。此外,在 Flask 中,我们需要一些样板代码来启动一个简单的应用程序。

由于我们将使用十二要素应用程序概念创建我们的应用程序,因此我们将首先确保我们有一个集中的代码库。到目前为止,您应该知道如何创建 GitHub 存储库。如果不知道,请确保按照第一章中提供的博客文章链接创建它,介绍云原生架构和微服务。我们将定期将代码推送到存储库。

假设您在本书的过程中已创建了存储库,我们将使用 GitHub 存储库 (github.com/PacktPublishing/Cloud-Native-Python.git)。

因此,让我们将本地目录与远程存储库同步。确保我们在 app 目录中,使用以下命令:

$ mkdir Cloud-Native-Python  # Creating the directory
$ cd Cloud-Native-Python  # Changing the path to working directory
$ git init . # Initialising the local directory
$ echo "Cloud-Native-Python" > README.md  # Adding description of repository
$ git add README.md  # Adding README.md
$ git commit -am "Initial commit"  # Committing the changes
$ git remote add origin https://github.com/PacktPublishing/Cloud-Native-Python.git  # Adding to local repository
$ git push -u origin master  # Pushing changes to remote repository.

您将看到以下输出:

我们已成功将第一个提交推送到远程存储库;我们将以类似的方式继续这样做,直到我们在构建微服务和应用程序方面达到一定的里程碑。

现在,我们需要安装一个基于文件的数据库,例如 SQLite 版本 3,它将作为我们微服务的数据存储。

要安装 SQLite 3,请使用以下命令:

$ apt-get install sqlite3 libsqlite3-dev -y

现在,我们可以创建并使用(源)virtualenv 环境,它将使本地应用程序的环境与全局 site-packages 安装隔离开来。如果未安装 virtualenv,可以使用以下命令进行安装:

$ pip install virtualenv

现在按如下方式创建virtualenv

$ virtualenv env --no-site-packages --python=python3
$ source env/bin/activate

我们应该看到上述命令的输出,如下面的截图所示:

virtualenv设置之后,当前,我们的virtualenv环境中需要安装一个依赖项。使用以下命令将一个包依赖项添加到requirements.txt中:

$ echo "Flask==0.10.1" >>  requirements.txt

将来,如果应用程序需要更多依赖项,它们将放在requirements.txt文件中。

让我们使用要求文件将依赖项安装到virtualenv环境中,如下所示:

$ pip install -r requirements.txt

现在我们已经安装了依赖项,让我们创建一个名为app.py的文件,其中包含以下内容:

    from flask import Flask 

    app = Flask(__name__) 

    if __name__ == "__main__": 
     app.run(host='0.0.0.0', port=5000, debug=True) 

上述代码是使用 Flask 运行应用程序的基本结构。它基本上初始化了Flask变量,并在端口5000上运行,可以从任何地方(0.0.0.0)访问。

现在,让我们测试上述代码,并查看一切是否正常工作。

执行以下命令来运行应用程序:

$ python app.py

我们应该看到上述命令的输出,如下面的截图所示:

此时,在我们开始构建 RESTful API 之前,我们需要决定我们的根 URL 是什么,以访问服务,这将进一步决定不同方法的子 URI。考虑以下示例:

http://[hostname]/api/v1/

由于在我们的情况下,我们将使用本地机器,hostname可以是带有端口的localhost,默认情况下,对于 Flask 应用程序,端口为5000。因此,我们的根 URL 将如下所示:

http://localhost:5000/api/v1/

现在,让我们决定对哪些资源执行不同的操作,并且这些资源将由此服务公开。在这种情况下,我们将创建两个资源:用户和推文。

我们的用户和信息资源将使用以下 HTTP 方法:

HTTP 方法 URI 操作
GET http://localhost:5000/api/v1/info 这将返回版本信息
GET http://localhost:5000/api/v1/users 这将返回用户列表
GET http://localhost:5000/api/v1/users/[user_id] 响应将是指定user_id的用户详细信息
POST http://localhost:5000/api/v1/users 此资源将在后端服务器中创建新用户,并使用传递的对象的值
DELETE http://localhost:5000/api/v1/users 此资源将删除以 JSON 格式传递的指定用户名的用户
PUT http://localhost:5000/api/v1/users/[user_id] 此资源将根据 API 调用的一部分传递的 JSON 对象更新特定user_id的用户信息。

使用客户端,我们将对资源执行操作,如addremovemodify等等。

在本章的范围内,我们将采用基于文件的数据库,如 SQLite 3,我们之前已经安装过。

让我们去创建我们的第一个资源,即/api/v1/info,并显示可用版本及其发布详细信息。

在此之前,我们需要创建一个apirelease表模式,如 SQLite 3 中定义的,其中将包含有关 API 版本发布的信息。可以按如下方式完成:

CREATE TABLE apirelease(
buildtime date,
version varchar(30) primary key,
links varchar2(30), methods varchar2(30));

创建后,您可以使用以下命令将记录添加到 SQLite 3 中的第一个版本(v1):

Insert into apirelease values ('2017-01-01 10:00:00', "v1", "/api/v1/users", "get, post, put, delete");

让我们在app.py中定义路由/api/v1/info和函数,它将基本上处理/api/v1/info路由上的 RESTful 调用。这样做如下:

    from flask import jsonify 
    import json 
    import sqlite3 
    @app.route("/api/v1/info") 
    def home_index(): 
      conn = sqlite3.connect('mydb.db') 
      print ("Opened database successfully"); 
      api_list=[] 
      cursor = conn.execute("SELECT buildtime, version,
      methods, links   from apirelease") 
    for row in cursor: 
        a_dict = {} 
        a_dict['version'] = row[0] 
        a_dict['buildtime'] = row[1] 
        a_dict['methods'] = row[2] 
        a_dict['links'] = row[3] 
        api_list.append(a_dict) 
    conn.close() 
    return jsonify({'api_version': api_list}), 200 

现在我们已经添加了一个路由和其处理程序,让我们在http://localhost:5000/api/v1/info上进行 RESTful 调用,如此截图所示:

太棒了!它有效了!

让我们继续讨论/api/v1/users资源,它将帮助我们对用户记录执行各种操作。

我们可以将用户定义为具有以下字段:

  • id:这是用户的唯一标识符(数字类型)

  • username:这是用户的唯一标识符或handler,用于身份验证(字符串类型)

  • emailid:这是用户的电子邮件(字符串类型)

  • password:这是用户的密码(字符串类型)

  • full_name:这是用户的全名(字符串类型)

为了在 SQLite 中创建用户表模式,请使用以下命令:

CREATE TABLE users( 
username varchar2(30), 
emailid varchar2(30), 
password varchar2(30), full_name varchar(30), 
id integer primary key autoincrement); 

构建资源用户方法

让我们为用户资源定义我们的GET方法。

GET /api/v1/users

GET/api/v1/users方法显示所有用户的列表。

app.py:
    @app.route('/api/v1/users', methods=['GET']) 
    def get_users(): 
      return list_users() 

现在我们已经添加了路由,我们需要定义list_users()函数,它将连接数据库以获取完整的用户列表。将以下代码添加到app.py中:

    def list_users():
    conn = sqlite3.connect('mydb.db')
    print ("Opened database successfully");
    api_list=[]
    cursor = conn.execute("SELECT username, full_name,
    email, password, id from users")
    for row in cursor:
    a_dict = {}
    a_dict['username'] = row[0]
    a_dict['name'] = row[1]
    a_dict['email'] = row[2]
    a_dict['password'] = row[3]
    a_dict['id'] = row[4]
    api_list.append(a_dict)
    conn.close()
      return jsonify({'user_list': api_list}) 

现在我们已经添加了路由和处理程序,让我们测试http://localhost:5000/api/v1/users URL,如下所示:

GET /api/v1/users/[user_id]

GET/api/v1/users/[user_id]方法显示由user_id定义的用户详细信息。

让我们创建一个将GET请求前置到app.py文件中的路由,如下所示:

   @app.route('/api/v1/users/<int:user_id>', methods=['GET']) 
   def get_user(user_id): 
     return list_user(user_id) 

如您在上面的代码中所看到的,我们将list_user(user_id)路由调用到list_user(user)函数中,但app.py中尚未定义。让我们定义它以获取指定用户的详细信息,如下所示,在app.py文件中:

    def list_user(user_id): 
      conn = sqlite3.connect('mydb.db') 
      print ("Opened database successfully"); 
      api_list=[] 
      cursor=conn.cursor() 
      cursor.execute("SELECT * from users where id=?",(user_id,)) 
      data = cursor.fetchall() 
      if len(data) != 0: 
         user = {} 
               user['username'] = data[0][0] 
         user['name'] = data[0][1] 
         user['email'] = data[0][2] 
         user['password'] = data[0][3] 
         user['id'] = data[0][4] 
            conn.close() 
            return jsonify(a_dict) 

现在我们已经添加了list_user(user_id)函数,让我们测试一下,看看是否一切正常:

糟糕!看来 ID 不存在;通常,如果 ID 不存在,Flask 应用程序会以404错误的 HTML 消息作出响应。由于这是一个 Web 服务应用程序,并且我们正在为其他 API 获取 JSON 响应,因此我们需要为404错误编写handler,以便即使对于错误,它也应该以 JSON 形式而不是 HTML 响应进行响应。例如,查看以下代码以处理404错误。现在,服务器将以代码的一部分作出适当的响应消息,如下所示:

    from flask import make_response 

    @app.errorhandler(404) 
    def resource_not_found(error): 
      return make_response(jsonify({'error':
      'Resource not found!'}),  404) 

此外,您可以从 Flask 中添加abort库,这基本上是用于调用异常。同样,您可以为不同的 HTTP 错误代码创建多个错误处理程序。

现在我们的GET方法运行良好,我们将继续编写POST方法,这类似于将新用户添加到用户列表中。

有两种方法可以将数据传递到POST方法中,如下所示:

  • JSON:在这种方法中,我们将 JSON 记录作为请求的一部分以对象的形式传递。RESTful API 调用将如下所示:
curl -i -H "Content-Type: application/json" -X POST -d {"field1":"value"} resource_url 

  • 参数化:在这种方法中,我们将记录的值作为参数传递,如下所示:
curl -i -H "Content-Type: application/json" -X POST resource_url?field1=val1&field2=val2 

在 JSON 方法中,我们以json的形式提供输入数据,并以相同的方式读取它。另一方面,在参数化方法中,我们以 URL 参数的形式提供输入数据(即username等),并以相同的方式读取数据。

还要注意,后端的 API 创建将根据所进行的 API 调用类型而有所不同。

POST /api/v1/users

在本书中,我们采用了POST方法的第一种方法。因此,让我们在app.py中定义post方法的路由,并调用函数将用户记录更新到数据库文件,如下所示:

    @app.route('/api/v1/users', methods=['POST']) 
    def create_user(): 
      if not request.json or not 'username' in request.json or not
      'email' in request.json or not 'password' in request.json: 
        abort(400) 
     user = { 
        'username': request.json['username'], 
        'email': request.json['email'], 
        'name': request.json.get('name',""), 
        'password': request.json['password'] 
     } 
      return jsonify({'status': add_user(user)}), 201 

在上面的方法中,我们使用错误代码400调用了异常;现在让我们编写它的处理程序:

    @app.errorhandler(400) 
    def invalid_request(error): 
       return make_response(jsonify({'error': 'Bad Request'}), 400) 

我们仍然需要定义add_user(user)函数,它将更新新的用户记录。让我们在app.py中定义它,如下所示:

    def add_user(new_user): 
     conn = sqlite3.connect('mydb.db') 
     print ("Opened database successfully"); 
     api_list=[] 
     cursor=conn.cursor() 
     cursor.execute("SELECT * from users where username=? or
      emailid=?",(new_user['username'],new_user['email'])) 
    data = cursor.fetchall() 
    if len(data) != 0: 
        abort(409) 
    else: 
       cursor.execute("insert into users (username, emailid, password,
   full_name) values(?,?,?,?)",(new_user['username'],new_user['email'],
    new_user['password'], new_user['name'])) 
       conn.commit() 
       return "Success" 
    conn.close() 
    return jsonify(a_dict) 

现在我们已经添加了handler,以及用户的POST方法的路由,让我们通过以下 API 调用来测试添加新用户:

curl -i -H "Content-Type: application/json" -X POST -d '{
"username":"mahesh@rocks", "email": "mahesh99@gmail.com",
"password": "mahesh123", "name":"Mahesh" }' 
http://localhost:5000/api/v1/users

然后,验证用户列表的 curl,http://localhost:5000/api/v1/users,如下截图所示:

DELETE /api/v1/users

delete方法帮助删除特定记录,该记录由用户名定义。我们将以 JSON 对象形式传递需要从数据库中删除的username

app.py for the DELETE method for users:
    @app.route('/api/v1/users', methods=['DELETE']) 
    def delete_user(): 
     if not request.json or not 'username' in request.json: 
        abort(400) 
     user=request.json['username'] 
      return jsonify({'status': del_user(user)}), 200 

del_user, which deletes the user record specified by username after validating whether it exists or not:
    def del_user(del_user): 
      conn = sqlite3.connect('mydb.db') 
      print ("Opened database successfully"); 
      cursor=conn.cursor() 
      cursor.execute("SELECT * from users where username=? ",
      (del_user,)) 
      data = cursor.fetchall() 
      print ("Data" ,data) 
      if len(data) == 0: 
        abort(404) 
      else: 
       cursor.execute("delete from users where username==?",
       (del_user,)) 
       conn.commit() 
         return "Success" 

太棒了!我们已经为用户资源的DELETE方法添加了路由/handler;让我们使用以下testAPI 调用来测试它:

    curl -i -H "Content-Type: application/json" -X delete -d '{ 
"username":"manish123" }' http://localhost:5000/api/v1/users

然后,访问用户列表 API(curl http://localhost:5000/api/v1/users)以查看是否已进行更改:

太棒了!用户删除成功。

PUT /api/v1/users

PUT API 基本上帮助我们更新由user_id指定的用户记录。

继续并创建一个使用PUT方法更新app.py文件中定义的user记录的路由,如下所示:

    @app.route('/api/v1/users/<int:user_id>', methods=['PUT']) 
    def update_user(user_id): 
     user = {} 
     if not request.json: 
         abort(400) 
     user['id']=user_id 
     key_list = request.json.keys() 
     for i in key_list: 
        user[i] = request.json[i] 
     print (user) 
     return jsonify({'status': upd_user(user)}), 200 

让我们指定upd_user(user)函数的定义,它基本上会更新数据库中的信息,并检查用户id是否存在:

    def upd_user(user): 
      conn = sqlite3.connect('mydb.db') 
      print ("Opened database successfully"); 
      cursor=conn.cursor() 
      cursor.execute("SELECT * from users where id=? ",(user['id'],)) 
      data = cursor.fetchall() 
      print (data) 
      if len(data) == 0: 
        abort(404) 
      else: 
        key_list=user.keys() 
        for i in key_list: 
            if i != "id": 
                print (user, i) 
                # cursor.execute("UPDATE users set {0}=? where id=? ",
                 (i, user[i], user['id'])) 
                cursor.execute("""UPDATE users SET {0} = ? WHERE id =
                ?""".format(i), (user[i], user['id'])) 
                conn.commit() 
        return "Success" 

现在我们已经为用户资源添加了PUT方法的 API 句柄,让我们按照以下方式进行测试:

我们已经定义了我们的资源,这是版本v1的一部分。 现在,让我们定义我们的下一个版本发布,v2,它将向我们的微服务添加一个推文资源。 在用户资源中定义的用户被允许对其推文执行操作。 现在,/api/info将显示如下:

我们的推文资源将使用以下HTTP方法:

HTTP 方法 URI 操作
GET http://localhost:5000/api/v2/tweets 这将检索推文列表
GET http://localhost:5000/api/v2/users/[user_id] 这将检索给定特定 ID 的推文
POST http://localhost:5000/api/v2/tweets 此资源将使用作为 API 调用的一部分传递的 JSON 数据在后端数据库中注册新推文

我们可以将推文定义为具有以下字段:

  • id:这是每条推文的唯一标识符(数字类型)

  • username:这应该作为用户存在于用户资源中(字符串类型)

  • body:这是推文的内容(字符串类型)

  • Tweet_time:(指定类型)

您可以在 SQLite 3 中定义前面的推文资源模式如下:

CREATE TABLE tweets( 
id integer primary key autoincrement, 
username varchar2(30), 
body varchar2(30), 
tweet_time date); 

太棒了!推文资源模式已准备就绪; 让我们为推文资源创建我们的GET方法。

构建资源推文方法

在本节中,我们将使用不同的方法为推文资源创建 API,这将帮助我们在后端数据库上执行不同的操作。

GET /api/v2/tweets

此方法列出所有用户的所有推文。

将以下代码添加到app.py中以添加GET方法的路由:

    @app.route('/api/v2/tweets', methods=['GET']) 
    def get_tweets(): 
      return list_tweets() 
    Let's define list_tweets() function which connects to database and
    get us all the tweets and respond back with tweets list 

   def list_tweets(): 
     conn = sqlite3.connect('mydb.db') 
     print ("Opened database successfully"); 
     api_list=[] 
     cursor = conn.execute("SELECT username, body, tweet_time, id from 
     tweets") 
    data = cursor.fetchall() 
    if data != 0: 
        for row in cursor: 
            tweets = {} 
            tweets['Tweet By'] = row[0] 
            tweets['Body'] = row[1] 
            tweets['Timestamp'] = row[2] 
    tweets['id'] = row[3] 
            api_list.append(tweets) 
    else: 
        return api_list 
    conn.close() 
    return jsonify({'tweets_list': api_list}) 

因此,现在我们已经添加了获取完整推文列表的功能,让我们通过以下 RESTful API 调用测试前面的代码:

目前,我们还没有添加任何推文,这就是为什么它返回了空集。 让我们添加一些推文。

POST /api/v2/tweets

POST 方法通过指定的用户添加新推文。

将以下代码添加到app.py中,以添加POST方法的路由到推文资源:

    @app.route('/api/v2/tweets', methods=['POST']) 
    def add_tweets(): 
      user_tweet = {} 
      if not request.json or not 'username' in request.json or not 
     'body' in request.json: 
        abort(400) 
    user_tweet['username'] = request.json['username'] 
    user_tweet['body'] = request.json['body'] 
    user_tweet['created_at']=strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) 
    print (user_tweet) 
    return  jsonify({'status': add_tweet(user_tweet)}), 200 

让我们添加add_tweet(user_tweet)的定义,以通过指定的用户添加推文,如下所示:

    def add_tweet(new_tweets): 
      conn = sqlite3.connect('mydb.db') 
      print ("Opened database successfully"); 
      cursor=conn.cursor() 
      cursor.execute("SELECT * from users where username=? ",
   (new_tweets['username'],)) 
    data = cursor.fetchall() 

    if len(data) == 0: 
        abort(404) 
    else: 
       cursor.execute("INSERT into tweets (username, body, tweet_time)
    values(?,?,?)",(new_tweets['username'],new_tweets['body'], 
    new_tweets['created_at'])) 
       conn.commit() 
       return "Success" 

因此,现在我们已经添加了将推文列表添加到数据库的功能,让我们通过以下 RESTful API 调用测试前面的代码:

curl -i -H "Content-Type: application/json" -X POST -d '{
"username":"mahesh@rocks","body": "It works" }' 
http://localhost:5000/api/v2/tweets  

我们应该看到前面的 API 调用的输出与以下截图类似:

让我们通过检查推文的状态来检查推文是否成功添加:

curl http://localhost:5000/api/v2/tweets -v

现在我们已经添加了我们的第一条推文,如果我们需要只看到特定 ID 的推文怎么办?在这种情况下,我们使用GET方法和user_id

GET /api/v2/tweets/[id]

GET方法列出由指定 ID 制作的推文。

将以下代码添加到app.py中,以添加具有指定 ID 的GET方法的路由:

    @app.route('/api/v2/tweets/<int:id>', methods=['GET']) 
    def get_tweet(id): 
      return list_tweet(id) 

让我们定义list_tweet()函数,它连接到数据库,获取具有指定 ID 的推文,并以 JSON 数据响应。 这样做如下:

     def list_tweet(user_id): 
       print (user_id) 
       conn = sqlite3.connect('mydb.db') 
       print ("Opened database successfully"); 
       api_list=[] 
      cursor=conn.cursor() 
      cursor.execute("SELECT * from tweets  where id=?",(user_id,)) 
      data = cursor.fetchall() 
      print (data) 
      if len(data) == 0: 
        abort(404) 
     else: 

        user = {} 
        user['id'] = data[0][0] 
        user['username'] = data[0][1] 
        user['body'] = data[0][2] 
        user['tweet_time'] = data[0][3] 

    conn.close() 
    return jsonify(user) 

现在我们已经添加了获取具有指定 ID 的推文的功能,让我们通过在以下位置进行 RESTful API 调用来测试前面的代码:

curl http://localhost:5000/api/v2/tweets/2

通过这些推文的添加,我们成功地构建了 RESTful API,它作为访问数据和执行各种操作所需的微服务共同工作。

测试 RESTful API

到目前为止,我们一直在构建 RESTful API 并访问根 URL 以查看响应,并了解不同的方法是否在后端正常工作。由于这是新代码,应该对所有内容进行 100%的测试,以确保它在生产环境中正常工作。在本节中,我们将编写测试用例,这些测试用例应该单独工作,也应该作为一个系统工作,以确保完整的后端服务可以投入生产。

有不同类型的测试,定义如下:

  • 功能测试:基本上用于测试组件或系统的功能。我们根据组件的功能规范进行此测试。

  • 非功能测试:这种测试针对组件的质量特征进行,包括效率测试、可靠性测试等。

  • 结构测试:这种类型的测试用于测试系统的结构。为了编写测试用例,测试人员需要了解代码的内部实现。

在本节中,我们将编写测试用例,特别是单元测试用例,针对我们的应用程序。我们将编写 Python 代码,它将自动运行,测试所有 API 调用,并以测试结果做出响应。

单元测试

单元测试是测试工作单元或被测试系统中的逻辑单元的代码片段。以下是单元测试用例的特点:

  • 自动化:它们应该自动执行

  • 独立:它们不应该有任何依赖关系

  • 一致和可重复:它们应该保持幂等性

  • 可维护:它们应该足够容易理解和更新

我们将使用一个名为nose的单元测试框架。作为替代,我们可以使用 docstest(https://docs.python.org/2/library/doctest.html)进行测试。

因此,让我们使用以下命令使用pip安装 nose:

$ pip install nose 

或者,您可以将其放在requirement.txt中,并使用以下命令进行安装:

$ pip install -r requirements.txt

现在我们已经安装了 nose 测试框架,让我们开始在一个单独的文件上编写初始测试用例,比如flask_test.py,如下所示:

    from app import app 
    import unittest 

   class FlaskappTests(unittest.TestCase): 
     def setUp(self): 
        # creates a test client 
        self.app = app.test_client() 
        # propagate the exceptions to the test client 
        self.app.testing = True 

上述代码将测试应用程序并使用我们的应用程序初始化self.app

让我们编写我们的测试用例,以获取GET /api/v1/users的响应代码,并将其添加到我们的 FlaskappTest 类中,如下所示:

    def test_users_status_code(self): 
        # sends HTTP GET request to the application 
        result = self.app.get('/api/v1/users') 
        # assert the status code of the response 
        self.assertEqual(result.status_code, 200) 

上述代码将测试我们是否在/api/v1/users上获得200的响应;如果没有,它将抛出错误,我们的测试将失败。正如你所看到的,由于这段代码没有任何来自其他代码的依赖,我们将其称为单元测试用例。

现在,如何运行这段代码?由于我们已经安装了 nose 测试框架,只需从测试用例文件的当前工作目录(在本例中为flask_test.py)中执行以下命令:

$ nosetests

太棒了!同样,让我们为本章前面创建的资源的不同方法的 RESTful API 编写更多的测试用例。

  • GET /api/v2/tweets测试用例如下:
    def test_tweets_status_code(self): 
        # sends HTTP GET request to the application 
        result = self.app.get('/api/v2/tweets') 
        # assert the status code of the response 
        self.assertEqual(result.status_code, 200) 

  • GET /api/v1/info测试用例如下:
    def test_tweets_status_code(self): 
        # sends HTTP GET request to the application 
        result = self.app.get('/api/v1/info') 
        # assert the status code of the response 
        self.assertEqual(result.status_code, 200) 

  • POST /api/v1/users测试用例写成这样:
    def test_addusers_status_code(self): 
        # sends HTTP POST request to the application 
        result = self.app.post('/api/v1/users', data='{"username":
   "manish21", "email":"manishtest@gmail.com", "password": "test123"}',
   content_type='application/json') 
        print (result) 
        # assert the status code of the response 
        self.assertEquals(result.status_code, 201) 

  • PUT /api/v1/users测试用例如下:
    def test_updusers_status_code(self): 
        # sends HTTP PUT request to the application 
        # on the specified path 
        result = self.app.put('/api/v1/users/4', data='{"password": 
   "testing123"}', content_type='application/json') 
        # assert the status code of the response 
        self.assertEquals(result.status_code, 200) 

  • POST /api/v1/tweets测试用例如下:
    def test_addtweets_status_code(self): 
        # sends HTTP GET request to the application 
        # on the specified path 
        result = self.app.post('/api/v2/tweets', data='{"username": 
   "mahesh@rocks", "body":"Wow! Is it working #testing"}', 
   content_type='application/json') 

        # assert the status code of the response 
        self.assertEqual(result.status_code, 201) 

  • DELETE /api/v1/users测试用例如下:
    def test_delusers_status_code(self): 
        # sends HTTP Delete request to the application 
        result = self.app.delete('/api/v1/users', data='{"username": 
   "manish21"}', content_type='application/json') 
        # assert the status code of the response 
        self.assertEquals(result.status_code, 200) 

同样,您可以根据自己的想法编写更多的测试用例,使这些 RESTful API 更加可靠和无错。

让我们一起执行所有这些测试,并检查是否所有测试都已通过。以下屏幕截图显示了对flask_test.py脚本的测试结果:

太棒了!现在我们所有的测试都已通过,我们可以继续创建围绕这些 RESTful API 的网页的下一个级别。

总结

在这一章中,我们专注于编写大量的代码来构建我们的微服务。我们基本上了解了 RESTful API 的工作原理。我们还看到了如何扩展这些 API,并确保我们理解这些 API 给出的HTTP响应。此外,您还学会了如何编写测试用例,这对于确保我们的代码能够正常运行并且适用于生产环境非常重要。

第三章:在 Python 中构建 Web 应用程序

在上一章中,我们专注于构建我们的微服务,即基本上是后端 RESTful API,并对其进行测试,以确保响应符合预期。到目前为止,我们一直在使用 curl 测试这些 RESTful API,或者使用测试框架,如 nose、unittest2 等。在本章中,我们将创建一些 HTML 页面,并编写一个 JavaScript REST 客户端,该客户端将与微服务进行交互。

本章中我们将涵盖的主题如下:

  • 构建 HTML 页面和数据绑定

  • 使用 knockout.js 的 JavaScript REST 客户端

在本章中,我们将创建一个客户端应用程序,该应用程序需要创建从 HTML 网页收集的动态内容,并根据用户的操作,将其作为对后端服务的响应进行更新。

作为开发人员,你一定遇到过许多采用 MVC 模式的应用程序框架。它是一个大类别,是MVCModel View Controller)、MVPModel View Presenter)和MVVMModel View ViewModel)的组合。

在我们的案例中,我们将使用knockout.js,这是一个基于 MVVM 模式的 JavaScript 库,它帮助开发人员构建丰富和响应式的网站。它可以作为独立使用,也可以与其他 JavaScript 库一起使用,如 jQuery。Knockout.js 将 UI 与底层 JavaScript 模型绑定在一起。模型根据 UI 的更改而更新,反之亦然,这基本上是双向数据绑定。

在 knockout.js 中,我们将处理两个重要的概念:绑定和 Observables。

Knockout.js 是一个通常用于开发类似桌面的 Web 应用程序的 JavaScript 库。它非常有用,因为它提供了一种与数据源同步的响应机制。它在数据模型和用户界面之间提供了双向绑定机制。在knockoutjs.com/documentation/introduction.html上阅读更多关于 knockout.js 的信息。

在本章中,我们将创建 Web 应用程序,以向数据库添加用户和推文,并对其进行验证。

开始使用应用程序

让我们开始创建一个基本的 HTML 模板。在应用程序根目录中创建一个名为template的目录;我们将在此目录中创建所有未来的模板。

现在,让我们按照以下方式为adduser.html文件创建基本骨架:

    <!DOCTYPE html> 
    <html> 
      <head> 
        <title>Tweet Application</title> 
      </head> 
      <body> 
        <div class="navbar"> 
         <div class="navbar-inner"> 
           <a class="brand" href="#">Tweet App Demo</a> 
         </div> 
        </div> 
       <div id="main" class="container"> 

         Main content here! 

       </div> 
      <meta name="viewport" content="width=device-width, initial-
       scale=1.0"> 
      <link href="http://netdna.bootstrapcdn.com/twitter-
       bootstrap/2.3.2/css/bootstrap-combined.min.css"
       rel="stylesheet"> 
      <script src="img/jquery- 
       1.9.0.js"></script> 
      <script src="img/twitter-
        bootstrap/2.3.2/js/bootstrap.min.js"></script> 
      <script src="img/knockout-
        2.2.1.js"></script> 
      </body> 
    </html> 

如你所见,在前面的代码中,我们指定了一些.js脚本,这些脚本是需要的,以使我们的 HTML 具有响应性。这类似于 twitter-bootstrap,它有一个<meta name="viewport">属性,可以根据浏览器尺寸来缩放页面。

创建应用程序用户

在我们开始编写网页之前,我们需要创建一个用于创建用户的路由,如下所示:

    from flask import render_template 

    @app.route('/adduser') 
    def adduser(): 
     return render_template('adduser.html') 

现在我们已经创建了路由,让我们在adduser.html中创建一个表单,该表单将要求用户提供与用户相关的必要信息,并帮助他们提交信息:

    <html> 
      <head> 
        <title>Twitter Application</title> 
      </head> 
      <body> 
       <form > 
         <div class="navbar"> 
          <div class="navbar-inner"> 
            <a class="brand" href="#">Tweet App Demo</a> 
          </div> 
        </div> 
        <div id="main" class="container"> 

         <table class="table table-striped"> 
           Name: <input placeholder="Full Name of user" type "text"/> 
           </div> 
           <div> 
             Username: <input placeholder="Username" type="username">
             </input> 
           </div> 
           <div> 
             email: <input placeholder="Email id" type="email"></input> 
           </div> 
           <div> 
             password: <input type="password" placeholder="Password">  
             </input> 
           </div> 
            <button type="submit">Add User</button> 
          </table> 
        </form> 
       <script src="img/
        jquery/1.8.3/jquery.min.js"></script> 
      <script src="img/knockout
        /2.2.0/knockout-min.js"></script> 
      <link href="http://netdna.bootstrapcdn.com/twitter-
       bootstrap/2.3.2/css/bootstrap-combined.min.css"
       rel="stylesheet"> 
      <!-- <script src="img/jquery-
       1.9.0.js"></script> --> 
     <script src="img/twitter- 
       bootstrap/2.3.2/js/bootstrap.min.js"></script> 
    </body> 
   </html> 

目前,前面的 HTML 页面只显示空字段,如果尝试提交带有数据的表单,它将无法工作,因为尚未与后端服务进行数据绑定。

现在我们准备创建 JavaScript,它将向后端服务发出 REST 调用,并添加来自 HTML 页面提供的用户内容。

使用 Observables 和 AJAX

为了从 RESTful API 获取数据,我们将使用 AJAX。Observables 跟踪数据的更改,并自动在所有使用和由ViewModel定义的位置上反映这些更改。

通过使用 Observables,使 UI 和ViewModel动态通信变得非常容易。

让我们创建一个名为app.js的文件,在静态目录中声明了 Observables,代码如下——如果目录不存在,请创建它:

    function User(data) { 
      this.id = ko.observable(data.id); 
      this.name = ko.observable(data.name); 
      this.username = ko.observable(data.username); 
      this.email = ko.observable(data.email); 
      this.password = ko.observable(data.password); 
    } 

    function UserListViewModel() { 
     var self = this; 
     self.user_list = ko.observableArray([]); 
     self.name = ko.observable(); 
     self.username= ko.observable(); 
     self.email= ko.observable(); 
     self.password= ko.observable(); 

     self.addUser = function() { 
      self.save(); 
      self.name(""); 
      self.username(""); 
      self.email(""); 
      self.password(""); 
     }; 
    self.save = function() { 
      return $.ajax({ 
      url: '/api/v1/users', 
      contentType: 'application/json', 
      type: 'POST', 
      data: JSON.stringify({ 
         'name': self.name(), 
         'username': self.username(), 
         'email': self.email(), 
         'password': self.password() 
      }), 
      success: function(data) { 
         alert("success") 
              console.log("Pushing to users array"); 
              self.push(new User({ name: data.name, username: 
              data.username,email: data.email ,password: 
               data.password})); 
              return; 
      }, 
      error: function() { 
         return console.log("Failed"); 
       } 
     }); 
    }; 
    } 

   ko.applyBindings(new UserListViewModel()); 

我知道这是很多代码;让我们了解前面代码的每个部分的用法。

当您在 HTML 页面上提交内容时,请求将在app.js接收,并且以下代码将处理请求:

    ko.applyBindings(new UserListViewModel()); 

它创建模型并将内容发送到以下函数:

    self.addUser = function() { 
      self.save(); 
      self.name(""); 
      self.username(""); 
      self.email(""); 
      self.password(""); 
   }; 

前面的addUser函数调用self.save函数,并传递数据对象。save函数通过 AJAX RESTful 调用后端服务,并执行从 HTML 页面收集的数据的POST操作。然后清除 HTML 页面的内容。

我们的工作还没有完成。正如我们之前提到的,这是双向数据绑定,因此我们需要从 HTML 端发送数据,以便在数据库中进一步处理。

在脚本部分中,添加以下行,它将识别.js文件路径:

    <script src="img/{{ url_for('static', filename='app.js') }}"></script> 

为 adduser 模板绑定数据

数据绑定对将数据与 UI 绑定很有用。如果我们不使用 Observables,UI 中的属性只会在第一次处理时被处理。在这种情况下,它无法根据底层数据更新自动更新。为了实现这一点,绑定必须引用 Observable 属性。

现在我们需要将我们的数据与表单及其字段绑定,如下面的代码所示:

    <form data-bind="submit: addUser"> 
     <div class="navbar"> 
       <div class="navbar-inner"> 
           <a class="brand" href="#">Tweet App Demo</a> 
       </div> 
     </div> 
     <div id="main" class="container"> 
      <table class="table table-striped"> 
       Name: <input data-bind="value: name" placeholder="Full Name of
       user" type "text"/> 
     </div> 
     <div> 
       Username: <input data-bind="value: username" 
       placeholder="Username" type="username"></input> 
     </div> 
    <div> 
      email: <input data-bind="value: email" placeholder="Email id" 
      type="email"></input> 
    </div> 
    <div> 
       password: <input data-bind="value: password" type="password" 
       placeholder="Password"></input> 
    </div> 
       <button type="submit">Add User</button> 
     </table> 
    </form> 

现在我们准备通过模板添加我们的用户。但是,我们如何验证用户是否成功添加到我们的数据库呢?一种方法是手动登录到数据库。但是,由于我们正在开发 Web 应用程序,让我们在网页上显示我们的数据(存在于数据库中)--甚至是新添加的条目。

为了读取数据库并获取用户列表,将以下代码添加到app.js中:

    $.getJSON('/api/v1/users', function(userModels) { 
      var t = $.map(userModels.user_list, function(item) { 
        return new User(item); 
      }); 
     self.user_list(t); 
    }); 

现在我们需要在adduser.html中进行更改,以显示我们的用户列表。为此,让我们添加以下代码:

    <ul data-bind="foreach: user_list, visible: user_list().length > 
    0"> 
      <li> 
        <p data-bind="text: name"></p> 
        <p data-bind="text: username"></p> 
        <p data-bind="text: email"></p> 
       <p data-bind="text: password"></p> 
     </li> 
    </ul> 

太棒了!我们已经完成了添加网页,它将为我们的应用程序创建新用户。它看起来会像这样:

从用户创建推文

在开始编写网页之前,我们需要创建一个用于创建推文的路由。可以按以下方式完成:

    from flask import render_template 

    @app.route('/addtweets') 
    def addtweetjs(): 
     return render_template('addtweets.html') 

现在,我们已经创建了路由,让我们在addtweets.html中创建另一个表单,该表单将要求用户提供与推文相关的必需信息,并帮助他们提交信息:

    <html> 
     <head> 
      <title>Twitter Application</title> 
     </head> 
    <body> 
    <form > 
     <div class="navbar"> 
       <div class="navbar-inner"> 
           <a class="brand" href="#">Tweet App Demo</a> 
       </div> 
      </div> 

      <div id="main" class="container"> 
       <table class="table table-striped"> 
         Username: <input placeholder="Username" type="username">
          </input> 
      </div> 
      <div> 
        body: <textarea placeholder="Content of tweet" type="text"> 
        </textarea> 
      </div> 
      <div> 
      </div> 
       <button type="submit">Add Tweet</button> 
      </table> 

     </form> 
      <script src="img/
       jquery/1.8.3/jquery.min.js"></script> 
      <script src="img/
        knockout/2.2.0/knockout-min.js"></script> 
       <link href="http://netdna.bootstrapcdn.com/twitter-
         bootstrap/2.3.2/css/bootstrap-combined.min.css" 
        rel="stylesheet"> 
      <!-- <script src="img/jquery-
        1.9.0.js"></script> --> 
      <script src="img/twitter-
        bootstrap/2.3.2/js/bootstrap.min.js"></script> 
     </body> 
    </html> 

请注意,当前此表单没有数据绑定以与 RESTful 服务通信。

使用 AJAX 处理 addtweet 模板的 Observables

让我们开发一个 JavaScript,它将对后端服务进行 REST 调用,并添加来自 HTML 页面的推文内容。

让我们在之前创建的静态目录中创建一个名为tweet.js的文件,并使用以下代码:

    function Tweet(data) { 
      this.id = ko.observable(data.id); 
      this.username = ko.observable(data.tweetedby); 
      this.body = ko.observable(data.body); 
      this.timestamp = ko.observable(data.timestamp); 
    } 

    function TweetListViewModel() { 
      var self = this; 
      self.tweets_list = ko.observableArray([]); 
      self.username= ko.observable(); 
      self.body= ko.observable(); 

      self.addTweet = function() { 
      self.save(); 
      self.username(""); 
      self.body(""); 
       }; 

      $.getJSON('/api/v2/tweets', function(tweetModels) { 
      var t = $.map(tweetModels.tweets_list, function(item) { 
        return new Tweet(item); 
      }); 
      self.tweets_list(t); 
      }); 

     self.save = function() { 
      return $.ajax({ 
      url: '/api/v2/tweets', 
      contentType: 'application/json', 
      type: 'POST', 
      data: JSON.stringify({ 
         'username': self.username(), 
         'body': self.body(), 
      }), 
      success: function(data) { 
         alert("success") 
              console.log("Pushing to users array"); 
              self.push(new Tweet({ username: data.username,body: 
              data.body})); 
              return; 
      }, 
      error: function() { 
         return console.log("Failed"); 
      } 
     }); 
      }; 
    } 

   ko.applyBindings(new TweetListViewModel()); 

让我们了解最后一段代码的每个部分的用法。

当您在 HTML 页面上提交内容时,请求将发送到tweet.js,代码的以下部分将处理请求:

    ko.applyBindings(new TweetListViewModel()); 

前面的代码片段创建模型并将内容发送到以下函数:

    self.addTweet = function() { 
      self.save(); 
      self.username(""); 
      self.body(""); 
      }; 

前面的addTweet函数调用self.save函数,并传递数据对象。保存函数通过 AJAX RESTful 调用后端服务,并执行从 HTML 页面收集的数据的POST操作。然后清除 HTML 页面的内容。

为了在网页上显示数据,并使其与后端服务中的数据保持同步,需要以下代码:

   function Tweet(data) { 
     this.id = ko.observable(data.id); 
     this.username = ko.observable(data.tweetedby); 
     this.body = ko.observable(data.body); 
     this.timestamp = ko.observable(data.timestamp); 
   } 

我们的工作还没有完成。正如我们之前提到的,这是双向数据绑定,因此我们还需要从 HTML 端发送数据,以便在数据库中进一步处理。

在脚本部分中,添加以下行,它将使用路径标识.js文件:

   <script src="img/{{ url_for('static', filename='tweet.js') }}"></script> 

为 addtweet 模板绑定数据

完成后,我们现在需要将我们的数据与表单及其字段绑定,如下面的代码所示:

    <form data-bind="submit: addTweet"> 
      <div class="navbar"> 
        <div class="navbar-inner"> 
           <a class="brand" href="#">Tweet App Demo</a> 
        </div> 
       </div> 
       <div id="main" class="container"> 

        <table class="table table-striped"> 
          Username: <input data-bind="value: username"
          placeholder="Username" type="username"></input> 
       </div> 
       <div> 
         body: <textarea data-bind="value: body" placeholder="Content
         of tweet" type="text"></textarea> 
       </div> 
       <div> 
       </div> 
       <button type="submit">Add Tweet</button> 
       </table> 
    </form> 

现在我们准备通过模板添加我们的推文。我们对推文进行验证,就像我们对用户进行验证一样。

为了读取数据库并获取推文列表,请将以下代码添加到tweet.js中:

    $.getJSON('/api/v2/tweets', function(tweetModels) { 
      var t = $.map(tweetModels.tweets_list, function(item) { 
      return new Tweet(item); 
     }); 
      self.tweets_list(t); 
     }); 

现在,我们需要在addtweets.html中进行更改,以显示我们的推文列表。为此,让我们添加以下代码:

    <ul data-bind="foreach: tweets_list, visible: tweets_list().length 
    > 0"> 
     <li> 
       <p data-bind="text: username"></p> 
       <p data-bind="text: body"></p> 
       <p data-bind="text: timestamp"></p> 

     </li> 
   </ul> 

太棒了!让我们来测试一下。它看起来会像这样:

以类似的方式,您可以通过从网页应用程序中删除用户或在后端服务中更新用户信息来扩展此用例。

此外,要了解更多关于 knockout.js 库的信息,请查看knockoutjs.com/examples/helloWorld.html上的实时示例,这将帮助您更好地理解,并帮助您在应用程序中实施它。

我们创建了这些网页,以确保我们的微服务工作,并让您了解通常如何开发 Web 应用程序;作为开发人员,我们也可以根据自己的用例创建这些 Web 应用程序。

CORS - 跨源资源共享

CORS 有助于在 API 请求的 API 服务器和客户端之间维护数据完整性。

使用 CORS 的想法是,服务器和客户端应该彼此具有足够的信息,以便它们可以相互验证,并使用 HTTP 标头在安全通道上传输数据。

当客户端发出 API 调用时,它要么是 GET 请求,要么是 POST 请求,其中 body 通常是 text/plain,带有名为Origin的标头--这包括与请求页面相关的协议、域名和端口。当服务器确认请求并发送响应以及Access-Control-Allow-Origin标头到相同的 Origin 时,它确保响应被正确接收到相应的 Origin。

通过这种方式,在不同来源之间进行资源共享。

几乎所有浏览器现在都支持 CORS,包括 IE 8+、Firefox 3.5+和 Chrome。

现在,既然我们已经准备好了 Web 应用程序,但它还没有启用 CORS,让我们启用它。

首先,您需要使用以下命令在 Flask 中安装 CORS 模块:

$pip install flask-cors

前面的包公开了一个 Flask 扩展,该扩展默认情况下在所有路由上为所有来源和方法启用 CORS 支持。安装了该包后,让我们在app.py中包含它,如下所示:

    from flask_cors import CORS, cross_origin 

要启用 CORS,您需要添加以下行:

   CORS(app) 

就是这样。现在,您的 Flask 应用程序中的所有资源都已启用 CORS。

如果您想在特定资源上启用 CORS,则添加以下代码与您的特定资源:

   cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) 

目前,我们还没有设置域,但我们正在本地主机级别工作。您可以通过在域名服务器中添加自定义域来测试 CORS,如下所示:

   127.0.0.1    <your-domain-name> 

现在,如果您尝试访问此<your-domain-name>,它应该能够正常使用此域名,并且您将能够访问资源。

会话管理

会话是与单个用户关联的一系列请求和响应事务。会话通常是通过对用户进行身份验证并跟踪他/她在网页上的活动来在服务器级别上维护的。

每个客户端的会话都分配了一个会话 ID。会话通常存储在 cookie 之上,并且服务器使用秘钥对它们进行加密--Flask 应用程序使用临时持续时间的秘钥对其进行解密。

目前,我们还没有设置身份验证--我们将在第八章中定义它,保护 Web 应用程序。因此,在这个时间点上,我们将通过询问访问网页的用户名并确保用户使用会话标识来创建会话。

现在让我们创建一个名为main.html的网页,其中将包含一个 URL 来创建会话(如果需要设置),以及用于在后端服务上执行操作的路由。如果会话已经存在,您可以清除会话。请参阅以下代码:

    <html> 
      <head> 
        <title>Twitter App Demo</title> 
        <link rel=stylesheet type=text/css href="{{ url_for('static', 
        filename='style.css') }}"> 
    </head> 
    <body> 
        <div id="container"> 
          <div class="title"> 
            <h1></h1> 
          </div> 
          <div id="content"> 
            {% if session['name'] %} 
            Your name seems to be <strong>{{session['name']}}</strong>.
           <br/> 
            {% else %} 
            Please set username by clicking it <a href="{{ 
            url_for('addname') }}">here</a>.<br/> 
            {% endif %} 
           Visit <a href="{{ url_for('adduser') }}">this for adding new 
           application user </a> or <a href="{{ url_for('addtweetjs') 
           }}">this to add new tweets</a> page to interact with RESTFUL
           API. 

           <br /><br /> 
           <strong><a href="{{ url_for('clearsession') }}">Clear 
           session</a></strong> 
            </div> 
            </div> 
        </div> 
       </body> 
    </html> 

当前在这个网页中,一些 URL,如clearsessionaddname不会工作,因为我们还没有为它们设置网页和路由。

另外,我们还没有为main.html网页设置路由;让我们首先在app.py中添加它,如下所示:

    @app.route('/') 

    def main(): 
      return render_template('main.html') 

由于我们已经为main.html设置了路由,让我们在app.py中为addname添加路由,如下所示:

   @app.route('/addname') 

   def addname(): 
   if request.args.get('yourname'): 
    session['name'] = request.args.get('yourname') 
    # And then redirect the user to the main page 
      return redirect(url_for('main')) 

    else: 
      return render_template('addname.html', session=session) 

正如您在前面的路由中所看到的,它调用了addname.html,而我们还没有创建它。让我们使用以下代码创建addname模板:

    <html> 
     <head> 
       <title>Twitter App Demo</title> 
       <link rel=stylesheet type=text/css href="{{ url_for('static', 
        filename='style.css') }}"> 
     </head> 
   <body> 
       <div id="container"> 
           <div class="title"> 
               <h1>Enter your name</h1> 
           </div> 
        <div id="content"> 
          <form method="get" action="{{ url_for('addname') }}"> 
            <label for="yourname">Please enter your name:</label> 
            <input type="text" name="yourname" /><br /> 
            <input type="submit" /> 
          </form> 
        </div> 
        <div class="title"> 
               <h1></h1> 
        </div> 
        <code><pre> 
        </pre></code> 
        </div> 
       </div> 
      </body> 
     </html> 

太棒了!现在我们可以使用前面的代码设置会话;您将看到一个类似于这样的网页:

现在,如果我们需要清除会话怎么办?由于我们已经从主网页调用了clearsession函数,我们需要在app.py中创建一个路由,进一步调用会话的Clear内置函数,如下所示:

    @app.route('/clear') 

     def clearsession(): 
      # Clear the session 
      session.clear() 
      # Redirect the user to the main page 
      return redirect(url_for('main')) 

这就是我们如何设置会话,为用户保持会话,并根据需要清除会话。

Cookies

Cookies 类似于会话,除了它们以文本文件的形式保存在客户端计算机上;而会话则保存在服务器端。

它们的主要目的是跟踪客户端的使用情况,并根据他们的活动通过了解 cookies 来改善体验。

cookies 属性存储在响应对象中,它是一个键值对的集合,其中包含 cookies、变量及其相应的值。

我们可以使用响应对象的set_cookie()函数设置 cookies,以存储 cookie,如下所示:

    @app.route('/set_cookie') 
    def cookie_insertion(): 
      redirect_to_main = redirect('/') 
      response = current_app.make_response(redirect_to_main )   
      response.set_cookie('cookie_name',value='values') 
      return response 

同样,读取 cookies 非常容易;get()函数将帮助您获取 cookies,如下所示:

    import flask 
    cookie = flask.request.cookies.get('my_cookie') 

如果 cookie 存在,它将被分配给 cookie,如果不存在,则 cookie 将返回None

摘要

在本章中,您学习了如何使用 JavaScript 库(如 knockout.js)将您的微服务与 Web 应用程序集成。您了解了 MVVM 模式,以及如何利用它们来创建完全开发的 Web 应用程序。您还学习了用户管理概念,如 cookies 和会话,以及如何利用它们。

在下一章中,我们将尝试通过将数据库从 SQLite 移动到其他 NoSQL 数据库服务(如 MongoDB)来加强和保护我们的数据库端。

第四章:交互数据服务

在上一章中,我们使用 JavaScript/HTML 构建了我们的应用程序,并将其与 RESTful API 和 AJAX 集成。您还学习了如何在客户端设置 cookie 和在服务器端设置会话,以提供更好的用户体验。在本章中,我们将专注于通过使用 NoSQL 数据库(如 MongoDB)而不是我们目前使用的 SQLite 数据库或 MySQL 数据库来改进我们的后端数据库,并将我们的应用程序与之集成。

本章将涵盖的主题如下:

  • 设置 MongoDB 服务

  • 将应用程序与 MongoDB 集成

MongoDB - 它的优势和我们为什么使用它?

在开始 MongoDB 安装之前,让我们了解为什么选择了 MongoDB 数据库以及它的需求。

让我们看看 MongoDB 相对于 RDBMS 的优势:

  • 灵活的模式:MongoDB 是一个文档数据库,一个集合可以包含多个文档。我们不需要在插入数据之前定义文档的模式,这意味着 MongoDB 根据插入文档的数据来定义文档的模式;而在关系型数据库中,我们需要在插入数据之前定义表的模式。

  • 较少的复杂性:在 MongoDB 中没有复杂的连接,就像在关系数据库管理系统中(例如:MySQL)数据库中一样。

  • 更容易扩展:与关系数据库管理系统相比,MongoDB 的扩展非常容易。

  • 快速访问:与 MySQL 数据库相比,MongoDB 中的数据检索速度更快。

  • 动态查询:MongoDB 支持对文档进行动态查询,它是一种基于文档的查询语言,这使其比其他关系型数据库(如 MySQL)更具优势。

以下是我们应该使用 MongoDB 的原因:

  • MongoDB 以 JSON 样式文档存储数据,这使得它很容易与应用程序集成。

  • 我们可以在任何文件和属性上设置索引

  • MongoDB 自动分片,这使得它易于管理并使其更快

  • MongoDB 在集群中使用时提供复制和高可用性

有不同的用例可以使用 MongoDB。让我们在这里检查它们:

  • 大数据

  • 用户数据管理

  • 内容交付和管理

以下图片显示了 MongoDB 与您的 Web 应用程序集成的架构图:

MongoDB 术语

让我们看看 MongoDB 的不同术语,接下来列出了它们:

  • 数据库:这类似于我们在关系数据库管理系统(RDBMS)中拥有的数据库,但是在 MongoDB 中,数据库是集合的物理容器,而不是表。MongoDB 可以有多个数据库。

  • 集合:这基本上是具有自己模式的文档的组合。集合不对文档的模式做出贡献。这与关系型数据库中的表相当。

  • 文档:这类似于关系数据库管理系统中的元组/行。它是一组键值对。它们具有动态模式,其中每个文档在单个集合中可能具有相同或不同的模式。它们也可能具有不同的字段。

以下代码是您理解的一个示例集合:

    {  
       _id : ObjectId(58ccdd1a19b08311417b14ee),  
       body : 'New blog post,Launch your app with the AWS Startup Kit!  
       #AWS', 
       timestamp : "2017-03-11T06:39:40Z", 
       id : 18, 
       tweetedby : "eric.strom" 
   } 

MongoDB 以一种名为BSON的二进制编码格式表示 JSON 文档。

设置 MongoDB

在当前情况下,我们正在使用 Ubuntu 工作站,因此让我们按照以下步骤在 Ubuntu 上安装 MongoDB。

我们将使用 Ubuntu 软件包管理工具,如apt,通过使用 GPG 密钥对经过分发者签名的软件包进行身份验证来安装 MongoDB 软件包。

要导入 GPG 密钥,请使用以下命令:

$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927

接下来,我们需要将 MongoDB 存储库路径设置为我们的操作系统,如下所示:

$ echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list

添加了这个之后,我们需要按照以下步骤更新我们的 Ubuntu 存储库:

$ sudo apt-get update  

现在存储库已更新,让我们使用以下命令安装最新的稳定 MongoDB 版本:

$ sudo apt-get install -y mongodb-org

安装后,MongoDB 服务应在端口27017上运行。我们可以使用以下命令检查服务状态:

$ sudo service mongodb status

如果它没有运行,您可以通过执行以下命令来启动服务:

$ sudo service mongodb start

太棒了!现在我们已经在本地机器上安装了 MongoDB。此时,我们只需要一个独立的 MongoDB 实例,但如果您想创建一个共享的 MongoDB 集群,那么可以按照以下链接中定义的步骤进行操作:

docs.mongodb.com/manual/tutorial/deploy-shard-cluster/

因此,现在我们已经在我们的机器上启用了 MongoDB 服务,我们可以开始在其上创建数据库。

初始化 MongoDB 数据库

以前,在 SQLite3 中创建数据库时,我们需要手动创建数据库并定义表的架构。由于 MongoDB 是无模式的,我们将直接添加新文档,并且集合将自动创建。在这种情况下,我们将仅使用 Python 初始化数据库。

在我们向 MongoDB 添加新文档之前,我们需要为其安装 Python 驱动程序,即pymongo

pymongo驱动程序添加到requirements.txt,然后使用pip软件包管理器进行安装,如下所示:

$echo "pymongo==3.4.0" >> requirements.txt
$ pip install -r requirements.txt

安装后,我们将通过在app.py中添加以下行来导入它:

from pymongo import MongoClient

现在我们已经为 Python 导入了 MongoDB 驱动程序,我们将在app.py中创建一个连接到 MongoDB 的连接,并定义一个函数,该函数将使用初始数据文档初始化数据库,如下所示:

    connection = MongoClient("mongodb://localhost:27017/") 
    def create_mongodatabase(): 
    try: 
       dbnames = connection.database_names() 
       if 'cloud_native' not in dbnames: 
           db = connection.cloud_native.users 
           db_tweets = connection.cloud_native.tweets 
           db_api = connection.cloud_native.apirelease 

           db.insert({ 
           "email": "eric.strom@google.com", 
           "id": 33, 
           "name": "Eric stromberg", 
           "password": "eric@123", 
           "username": "eric.strom" 
           }) 

           db_tweets.insert({ 
           "body": "New blog post,Launch your app with the AWS Startup
           Kit! #AWS", 
           "id": 18, 
           "timestamp": "2017-03-11T06:39:40Z", 
           "tweetedby": "eric.strom" 
           }) 

           db_api.insert( { 
             "buildtime": "2017-01-01 10:00:00", 
             "links": "/api/v1/users", 
             "methods": "get, post, put, delete", 
             "version": "v1" 
           }) 
           db_api.insert( { 
             "buildtime": "2017-02-11 10:00:00", 
             "links": "api/v2/tweets", 
             "methods": "get, post", 
             "version": "2017-01-10 10:00:00" 
           }) 
           print ("Database Initialize completed!") 
       else: 
           print ("Database already Initialized!")
       except: 
           print ("Database creation failed!!") 

建议您使用一些文档初始化资源集合,以便在开始测试 API 时获得一些响应数据,否则,您可以在不初始化集合的情况下继续。

在启动应用程序之前应调用上述函数;我们的主要函数将如下所示:

   if __name__ == '__main__': 
     create_mongodatabase() 
     app.run(host='0.0.0.0', port=5000, debug=True) 

将微服务与 MongoDB 集成

由于我们已经初始化了 MongoDB 数据库,现在是时候重写我们的微服务函数,以便从 MongoDB 而不是 SQLite 3 中存储和检索数据。

以前,我们使用curl命令从 API 获取响应;而现在,我们将使用一个名为POSTMANwww.getpostman.com)的新工具,该工具是一个可以帮助您更快地构建、测试和记录 API 的应用程序。

有关 POSTMAN 工作原理的更多信息,请阅读以下链接的文档:www.getpostman.com/docs/

POSTMAN 支持 Chrome 和 Firefox,因为它可以很容易地集成为一个附加组件。

首先,我们将修改api_version信息 API,以从 MongoDB 中收集信息,而不是从 SQLite3 中收集,如下所示:

    @app.route("/api/v1/info") 
    def home_index(): 
     api_list=[] 
     db = connection.cloud_native.apirelease 
     for row in db.find(): 
       api_list.append(str(row)) 
     return jsonify({'api_version': api_list}), 200 

现在,如果您使用 POSTMAN 进行测试,它应该会给出类似于以下内容的输出:

太棒了!它有效。现在,让我们更新微服务的其他资源。

处理用户资源

我们将按以下方式修改app.py中不同方法的用户资源 API 函数。

GET api/v1/users

GET API 函数获取完整的用户列表。

为了从 MongoDB 数据库中获取完整的用户列表,我们将按以下方式重写list_users()函数:

    def list_users(): 
     api_list=[] 
     db = connection.cloud_native.users 
     for row in db.find(): 
       api_list.append(str(row)) 
     return jsonify({'user_list': api_list}) 

让我们在 POSTMAN 上进行测试,看看 API 是否按预期响应:

由于我们目前在 MongoDB 数据库的用户集合中只有一个文档,因此在上述屏幕截图中只能看到一个用户。

GET api/v1/users/[user_id]

此 API 函数获取特定用户的详细信息。

为了从 MongoDB 数据库中列出特定用户的详细信息,请使用以下方式调用modify list_user(user_id)函数:

    def list_user(user_id): 
     api_list=[] 
     db = connection.cloud_native.users 
     for i in db.find({'id':user_id}): 
       api_list.append(str(i)) 

     if api_list == []: 
       abort(404) 
     return jsonify({'user_details':api_list} 

让我们在 POSTMAN 上测试一下,看看它是否按预期工作:

此外,我们需要测试用户条目不存在的情况;请尝试以下代码:

POST api/v1/users

该 API 函数用于将新用户添加到用户列表中。

在这段代码中,我们将重写add_user(new_user)函数与 MongoDB 进行交互,将用户添加到用户集合中:

    def add_user(new_user): 
     api_list=[] 
     print (new_user) 
     db = connection.cloud_native.users 
     user = db.find({'$or':[{"username":new_user['username']}     ,  
    {"email":new_user['email']}]}) 
     for i in user: 
       print (str(i)) 
       api_list.append(str(i)) 

     if api_list == []: 
       db.insert(new_user) 
       return "Success" 
     else : 
       abort(409) 

现在我们已经修改了我们的函数,还有一件事需要做——之前,ID 是由 SQLite 3 生成的,但现在,我们需要通过将其添加到其路由函数中使用随机模块来生成它们,如下所示:

    def create_user(): 
     if not request.json or not 'username' in request.json or not 
    'email' in request.json or not 'password' in request.json: 
       abort(400) 
     user = { 
       'username': request.json['username'], 
       'email': request.json['email'], 
       'name': request.json.get('name',""), 
       'password': request.json['password'], 
       'id': random.randint(1,1000) 
     } 

让我们向用户列表添加一条记录,以测试它是否按预期工作。

以下截图显示了在 MongoDB 中使用 POSTMAN 添加新记录的输出状态:

让我们验证是否已在 MongoDB 集合中更新了属性。

以下截图验证了我们的新记录已成功添加:

PUT api/v1/users/[user_id]

该 API 函数用于更新 MongoDB 用户集合中用户的属性。

为了更新 MongoDB 用户集合中特定用户的文档,我们需要将upd_user(user)方法重写如下:

    def upd_user(user): 
     api_list=[] 
     print (user) 
     db_user = connection.cloud_native.users 
     users = db_user.find_one({"id":user['id']}) 
     for i in users: 
       api_list.append(str(i)) 
      if api_list == []: 
       abort(409) 
      else: 
       db_user.update({'id':user['id']},{'$set': user}, upsert=False ) 
       return "Success" 

现在我们已经更新了方法,让我们在 POSTMAN 上测试一下,并检查响应。

以下截图显示了使用 POSTMAN 进行更新 API 请求的响应:

让我们验证用户文档,检查字段是否已修改:

DELETE api/v1/users

该 API 从用户列表中删除特定用户。

在这种情况下,我们将修改del_user(del_user)方法,以从 MongoDB 用户集合中删除用户,如下所示:

    def del_user(del_user): 
     db = connection.cloud_native.users 
    api_list = [] 
    for i in db.find({'username':del_user}): 
       api_list.append(str(i)) 

     if api_list == []: 
       abort(404) 
    else: 
      db.remove({"username":del_user}) 
      return "Success" 

让我们在 POSTMAN 上测试一下,看看响应是否符合预期:

现在我们已经删除了一个用户,让我们看看是否对整体用户列表造成了任何更改:

太棒了!我们已经对用户资源的所有 RESTful API URL 进行了更改,并进行了验证。

处理推文资源

现在我们的用户资源 API 在 MongoDB 作为数据库服务上运行良好,我们将对推文资源做同样的操作。

GET api/v2/tweets

此函数从所有用户获取所有推文的完整列表。

让我们更新我们的list_tweets()方法,开始使用以下代码片段从 MongoDB 的推文集合中获取推文列表:

def list_tweets(): 
   api_list=[] 
   db = connection.cloud_native.tweet 
   for row in db.find(): 
       api_list.append(str(row)) 
   return jsonify({'tweets_list': api_list}) 

现在我们已经更新了代码,让我们在 POSTMAN 上测试一下。以下截图列出了通过 POSTMAN 使用 API 请求的所有推文:

GET api/v2/tweets/[user_id]

此函数从特定用户获取推文。

为了从推文集合中获取特定用户的推文,我们需要修改我们当前的list_tweet(user_id)函数如下:

    def list_tweet(user_id): 
     db = connection.cloud_native.tweets 
     api_list=[] 
     tweet = db.find({'id':user_id}) 
     for i in tweet: 
       api_list.append(str(i)) 
    if api_list == []: 
       abort(404) 
    return jsonify({'tweet': api_list}) 

让我们测试一下我们的 API,并验证它是否按预期工作:

POST api/v2/tweets

此函数从现有用户添加新推文。

在这种情况下,我们需要修改我们的add_tweet(new_tweet)方法与用户进行交互,并在 MongoDB 中的推文集合中添加新推文,如下所示:

    def add_tweet(new_tweet): 
     api_list=[] 
     print (new_tweet) 
     db_user = connection.cloud_native.users 
     db_tweet = connection.cloud_native.tweets 
     user = db_user.find({"username":new_tweet['tweetedby']}) 
     for i in user: 
       api_list.append(str(i)) 
     if api_list == []: 
      abort(404) 
     else: 
       db_tweet.insert(new_tweet) 
       return "Success" 

现在我们已经修改了记录,让我们测试一下。以下截图显示了使用 POSTMAN 添加新推文的POST请求的成功状态:

现在让我们验证新添加的推文是否在推文列表中更新,如下截图所示:

总结

在本章中,我们将基于文件的数据库服务(SQLite)迁移到 NoSQL 文档型数据库服务(MongoDB)。您将学习如何将 MongoDB 与您的 RESTful API 集成,以响应客户端的请求并保存数据。下一章将更有趣,因为我们将使用 React 构建我们的前端 Web 视图。

第五章:使用 React 构建 WebViews

到目前为止,我们一直在构建我们的微服务,并使我们的后端服务更加响应和高效。此外,我们一直在尝试不同的数据库服务,这些服务可以保护并提高数据的存储和检索性能,这在这里是至关重要的。

在本章中,我们将专注于使用 React 构建我们的前端页面,并将这些页面与后端集成以形成一个完整的应用程序。

本章将涵盖的主题如下:

  • 设置 React 环境

  • 创建用户认证面板

  • 将 React 与后端 API 集成

理解 React

简单来说,React 是你的应用程序的 UI 层。它是一个用于构建快速和快速用户界面的 JavaScript 库。React 基本上帮助你为你的应用程序的每个状态创建令人惊叹的 web 视图。因此,我们将使用 React 来实现这个目的。但在我们这样做之前,让我们了解 React 的一些概念/关键点,下面列出了这些概念/关键点:

  • 组件:你的 HTML 和 JavaScript 的所有集合都被称为组件。React 基本上提供了渲染启用 JavaScript 的 HTML 页面的钩子。这里的重要一点是,React 作为控制器,用于渲染应用程序的不同状态的不同网页。

  • React 中静态版本的 Props:通常,在 HTML 中,你需要大量的代码来在前端显示所有数据,而且这是重复的。React 的 props 帮助你解决了这个问题。props 基本上保持数据的状态,并从父级传递值给子级。

  • 识别最小状态:为了正确构建你的应用程序,你首先需要考虑你的应用程序需要的最小可变状态集。比如,在我们的情况下,我们需要在应用程序的不同状态下始终保持用户状态可用。

  • 识别活动状态:React 的核心是组件层次结构中的单向数据流。我们需要了解每个基于该状态渲染内容的组件。此外,我们需要了解组件层次结构中状态如何改变。

  • React-DOM:react-dom 是 React 和 DOM 的组合。React 包含在 Web 和移动应用程序中使用的功能。react-dom 功能仅在 Web 应用程序中使用。

设置 React 环境

为了运行 React,我们需要设置一个初始环境,其中包括安装一些node.js的库。

安装 node

在开始安装 React 和包列表之前,我们需要在系统上安装node.js

在 Linux(基于 Debian 的系统)中,安装过程非常简单。

首先,我们需要使用以下命令从node.js官方网站添加 PPA:

$ sudo apt-get install python-software-properties
$ curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash -

一旦设置好,我们可以使用以下命令安装node.js

$ apt-get install nodejs 

现在让我们检查nodenpm的版本,如下所示:

$ npm -v
 4.1.2 
$ node -v
  V7.7.4 

在我们的设置中,我们使用了上述版本,但是 v7.x 左右的 node 版本应该可以,对于 npm,v4.x 应该可以。

创建 package.json

这个文件基本上是你的应用程序的元数据,其中包含需要为你的应用程序安装的完整库/依赖项。另一个现实世界的优势是,它使你的构建可复制,这意味着与其他开发人员分享变得更容易。有不同的方式可以创建你定制的package.json

以下是在packages.json中需要提供的最少信息:

    "Name" - lowercase.
    "version"  - in the form of x.x.x

    For example:

    {
      "name": "my-twitter-package",
      "version": "1.0.0"
    } 

为了创建package.json模板,你可以使用以下命令:

$ npm init              # in your workspace  

它会要求填写诸如名称、版本、描述、作者、许可证等值;填写这些值,它将生成package.json

如果你现在不想填写信息,你可以使用--yes-y属性使用默认值,如下所示:

$npm init --yes

对于我们的应用程序,我已经生成了类似以下内容的package.json

    { 
      "name": "twitter", 
      "version": "1.0.0", 
      "description": "Twitter App", 
      "main": "index.js", 
      "dependencies": { 
        "babel-loader": "⁶.4.1", 
        "fbjs": "⁰.8.11", 
        "object-assign": "⁴.1.1", 
        "react": "¹⁵.4.2", 
        "react-dev": "0.0.1", 
        "react-dom": "⁰.14.7", 
        "requirejs": "².3.3" 
      }, 
     "devDependencies": { 
       "babel-core": "⁶.4.5", 
       "babel-loader": "⁶.2.1", 
       "babel-preset-es2015": "⁶.3.13", 
       "babel-preset-react": "⁶.3.13", 
       "webpack": "¹.12.12" 
      }, 
    "scripts": { 
      "test": "echo \"Error: no test specified\" && exit 1" 
     }, 
    "author": "Manish Sethi", 
    "license": "ISC" 
   } 

现在,我们已经生成了package.json,我们需要使用以下命令在我们的工作站上安装这些依赖项:

$ npm install 

请确保在执行上述命令时,package.json应该在当前工作目录中。

使用 React 构建 webViews

首先,我们将创建一个主页视图,从中将调用 React。所以,让我们创建index.html,它在模板目录中有以下内容:

    <!DOCTYPE html> 
    <html> 
     <head lang="en"> 
      <meta charset="UTF-8"> 
      <title>Flask react</title> 
    </head> 
   <body> 
     <div class="container"> 
       <h1></h1> 
       <br> 
       <div id="react"></div> 

    </div> 

   <!-- scripts --> 
    <script src="img/jquery-2.1.1.min.js"></script> 
    <script src="img/
      react/15.1.0/react.min.js"></script> 
    <script src="img/react-
      router@2.8.1/umd/ReactRouter.min.js"></script> 
    <script src="img/
      libs/react/15.1.0/react-dom.min.js"></script> 
    <script src="img/
      react/0.13.3/JSXTransformer.js"></script> 

    </body> 
   </html> 

正如你在前面的 HTML 页面中所看到的,我们已经定义了id="react",我们将使用它来根据 ID 调用 React 的主要函数,并执行某些操作。

所以,让我们创建我们的main.js,它将发送一个响应,代码如下:

    import Tweet from "./components/Tweet"; 
    class Main extends React.Component{ 
    render(){ 
      return ( 
      <div> 
        <h1>Welcome to cloud-native-app!</h1> 
      </div> 
      ); 
    } 
   } 

   let documentReady =() =>{ 
    ReactDOM.render( 
    <Main />, 
     document.getElementById('react') 
    ); 
  }; 

  $(documentReady); 

现在我们已经定义了 React 响应的基本结构。由于我们正在构建具有多个视图的应用程序,我们需要一个构建工具,它将帮助我们将所有资产,包括 JavaScript、图像、字体和 CSS,放在一个包中,并将其生成为一个单一的文件。

Webpack是将帮助我们解决这个问题的工具。

Webpack 应该已经可用,因为我们在package.json中定义了 Webpack 包,我们之前安装过了。

Webpack 基本上读取一个入口文件,它可以是.js文件,读取它的子组件,然后将它们转换成一个单一的.js文件。

由于我们已经在package.json中定义了它,它已经安装好了。

在 Webpack 中,我们需要定义一个配置,它将帮助它识别入口文件和要用于生成单一.js文件的加载器。此外,你需要定义生成代码的文件名。

我们的 Webpack 配置应该是这样的:

    module.exports = { 
      entry: "./static/main.js", 
      output: { 
        path: __dirname + "/static/build/", 
        filename: "bundle.js" 
      }, 
     resolve: { 
       extensions: ['', '.js', '.jsx'] 
     }, 
     module: { 
        loaders: [ 
            { test: /\.js$/, exclude: /node_modules/, loader: "babel-
        loader", query:{presets:['react','es2015']} } 
        ] 
     } 
   }; 

你可以根据你的用例扩展前面的配置。有时,开发人员尝试使用*.html 作为入口点。在这种情况下,你需要做出适当的更改。

让我们继续使用以下命令构建我们的第一个 webView:

$ webpack -d  

最后一个命令中的-d属性用于调试;它生成另一个文件bundle.js.map,显示 Webpack 的活动。

由于我们将重复构建应用程序,我们可以使用另一个标志--watch-w,它将跟踪main.js文件的更改。

所以,现在我们的 Webpack 命令应该是这样的:

$ webpack -d -w

现在我们已经构建了我们的应用程序。记得在app.py中更改你的路由,这样主页应该被导航如下:

    @app.route('/index') 
    def index(): 
     return render_template('index.html') 

让我们检查一下我们的主页现在是什么样子的。

你也可以检查一下我们是否在检查模式下后台运行着 React 和 react-dom。

这是一个非常基本的结构,用于理解 React 的工作原理。让我们继续我们的用例,我们已经创建了 tweet webViews,用户也可以查看旧的 tweets。

所以,让我们创建Tweet.js,它将有 tweets 的基本结构,比如一个用于内容的文本框,和一个用于发布 tweets 的按钮。将以下代码添加到Tweet.js

    export default class Tweet extends React.Component { 

    render(){ 
     return( 
        <div className="row"> 
                </nav> 
        <form > 
          <div > 
            <textarea ref="tweetTextArea" /> 
            <label>How you doing?</label> 
              <button >Tweet now</button> 
          </div> 
         </form> 
        </div> 
        ); 
      } 
   } 

让我们从main.js中调用这个函数,这样它就会在主页上加载,通过更新render函数如下:

    import Tweet from "./components/Tweet"; 
    render(){ 
      return ( 
      <div> 
        <Tweet /> 
      </div> 
     ); 
    } 

如果你现在加载页面,它将会非常简单。由于我们想要创建一个吸引人的 web 应用程序,我们将在这里使用一些 CSS 来实现。在我们的情况下,我们使用 Materialize CSS (materializecss.com/getting-started.html)。

index.html中添加以下代码块:

    <link rel="stylesheet"  
      href="https://cdnjs.cloudflare.com/ajax/libs/
      materialize/0.98.1/css/materialize.min.css"> 
   <script src="img/
     materialize/0.98.1/js/materialize.min.js"></script> 

   Also, we need to update Tweet.js as follows 

   render(){ 
    return( 
        <div className="row"> 
         <form > 
          <div className="input-field"> 
            <textarea ref="tweetTextArea" className="materialize-
             textarea" /> 
            <label>How you doing?</label> 
              <button className="btn waves-effect waves-light
               right">Tweet now <i className="material-icons 
               right">send</i></button> 
          </div> 
         </form> 
       </div> 
      ); 
    } 

让我们尝试添加 tweets,并通过状态发送它们,以便显示一些 tweets。

main.jsMain类中,添加以下构造函数来初始化状态:

    constructor(props){ 
     super(props); 
     this.state =  { userId: cookie.load('session') }; 
     this.state={tweets:[{'id': 1, 'name': 'guest', 'body': '"Listen to 
     your heart. It knows all things." - Paulo Coelho #Motivation' }]} 
    } 

现在按照以下方式更新render函数:

    render(){ 
      return ( 
      <div> 
         <TweetList tweets={this.state.tweets}/> 
      </div> 
      ); 
     } 
    } 

让我们创建另一个文件TweetList.js,它将显示 tweets,代码如下:

    export default class TweetList extends React.Component { 
     render(){ 
        return( 
        <div> 
          <ul className="collection"> 
           <li className="collection-item avatar"> 
           <i className="material-icons circle red">play_arrow</i> 
           <span className="title">{this.props.tweetedby}</span> 
          <p>{this.props.body}</p> 
          <p>{this.props.timestamp}</p> 
          </li> 
         </ul> 
        </div> 
       ); 
      } 
     } 

太棒了!现在我们已经添加了这个模板。让我们检查一下我们的主页,看看 CSS 是如何工作的。但在此之前,由于我们正在使用 Webpack 进行构建,请确保每次都添加以下行以加载bundle.js-这将在index.html文件中运行 webView。

    <script type="text/javascript" src="img/bundle.js">
     </script> 

太棒了!主页应该是这样的:

让我们继续发布推文-我们应该能够添加新的推文,并且它们也应该在TweetList.js中更新。

让我们更新我们的Tweet.js代码,以便将推文发送到main.js进行处理。现在,我们需要将我们的推文发送到main.js,为此,我们需要使用以下代码更新我们的Tweet.js文件:

    sendTweet(event){ 
      event.preventDefault(); 
      this.props.sendTweet(this.refs.tweetTextArea.value); 
      this.refs.tweetTextArea.value = ''; 
     } 

还要确保使用以下form onSubmit属性更新render函数:

    <form onSubmit={this.sendTweet.bind(this)}> 

因此,在向文本区域添加内容后,它还应该提交推文。

现在,让我们更新main.jsrender函数以添加新的推文,如下所示:

    <Tweet sendTweet={this.addTweet.bind(this)}/> 

我们还需要在以下定义的Main类中添加addTweet函数:

    addTweet(tweet): 
     let newTweet = this.state.tweets; 
     newTweet.unshift({{'id': Date.now(), 'name': 'guest','body':
      tweet}) 
     this.setState({tweets: newTweet}) 

在添加新推文后,您的页面应该看起来像这样:

目前,我们正在使用 React 来保存数组中的数据。由于我们已经构建了我们的微服务来保存这种数据,我们应该将我们的 webView 与后端服务集成。

将 webView 与微服务集成

为了将我们的微服务与 webView 集成,我们将使用 AJAX 进行 API 调用。

main.js to pull our entire tweet list:
    componentDidMount() { 
      var self=this; 
      $.ajax({url: `/api/v2/tweets/`, 
      success: function(data) { 
        self.setState({tweets: data['tweets_list']}); 
        alert(self.state.tweets); 
        return console.log("success"); 
       }, 
     error: function() { 
      return console.log("Failed"); 
      } 
    }); 

同样,我们需要修改我们main.js中的addTweet函数,如下所示:

   addTweet(tweet){ 
     var self = this; 
     $.ajax({ 
       url: '/api/v2/tweets/', 
       contentType: 'application/json', 
       type: 'POST', 
       data: JSON.stringify({ 
         'username': "Agnsur", 
      'body': tweet, 
       }), 
       success: function(data) { 
            return console.log("success"); 
       }, 
       error: function() { 
         return console.log("Failed"); 
       } 
     }); 
    } 

由于将有多条推文需要使用相似的推文模板进行迭代,让我们创建另一个名为templatetweet.js的组件,并使用以下代码:

    export default class Tweettemplate extends React.Component { 
     render(props){ 
      return( 
      <li className="collection-item avatar"> 
        <i className="material-icons circle red">play_arrow</i> 
        <span className="title">{this.props.tweetedby}</span> 
        <p>{this.props.body}</p> 
        <p>{this.props.timestamp}</p> 
      </li> 

      ); 
     } 
    } 

请记住,我们已根据我们的数据库集合键更改了 props 字段。

此外,我们需要更新我们的TweetList.js,以使用前面的模板,通过以下方式添加它:

    import Tweettemplate from './templatetweet' 

    export default class TweetList extends React.Component { 
    render(){ 
     let tweetlist = this.props.tweets.map(tweet => <Tweettemplate key=
     {tweet.id} {...tweet} />); 
    return( 
        <div> 
          <ul className="collection"> 
            {tweetlist} 
          </ul> 
        </div> 
      ); 
     } 
    } 

太棒了!您的主页现在应该是这样的:

用户身份验证

我们所有的推文都是受保护的,应该只对我们想展示给他们的观众做出反应。此外,匿名用户不应被允许发推文。为此,我们将创建一个数据库和网页,以使新用户能够登录并在推文 webView 中登录。请记住,我们将使用 Flask 来验证用户,并将数据发布到后端用户。

登录用户

让我们创建我们的登录页面模板,现有用户需要填写他们的用户名和密码进行身份验证。以下是代码片段:

    <form action="/login" method="POST"> 
     <div class="login"> 
     <div class="login-screen"> 
     <div class="app-title"> 
      <h1>Login</h1> 
     </div> 

     <div class="login-form"> 
     <div class="control-group"> 

      <input type="text" class="login-field" value="" 
       placeholder="username" name="username"> 
      <label class="login-field-icon fui-user" for="login-name">
      </label> 
     </div> 

    <div class="control-group"> 
      <input type="password" class="login-field" value=""
       placeholder="password" name="password"> 
      <label class="login-field-icon fui-lock" for="login-pass">
      </label> 
    </div> 
     <input type="submit" value="Log in" class="btn btn-primary btn-
     large btn-block" ><br> 
     Don't have an account? <a href="{{ url_for('signup') }}">Sign up
     here</a>. 
   </div> 

我们将向登录页面发布数据,我们将在app.py文件中定义。

但首先,检查会话是否存在。如果没有,那么您将被重定向到登录页面。将以下代码添加到app.py中,它将验证用户的会话详细信息:

   @app.route('/') 
   def home(): 
     if not session.get('logged_in'): 
        return render_template('login.html') 
     else: 
        return render_template('index.html', session =   
     session['username']) 

让我们为登录创建路由,并验证凭据以对用户进行身份验证。

以下是代码片段:

    @app.route('/login', methods=['POST']) 
    def do_admin_login(): 
      users = mongo.db.users 
      api_list=[] 
      login_user = users.find({'username': request.form['username']}) 
      for i in login_user: 
        api_list.append(i) 
      print (api_list) 
      if api_list != []: 
         if api_list[0]['password'].decode('utf-8') == 
         bcrypt.hashpw(request.form['password'].encode('utf-8'), 
         api_list[0]['password']).decode('utf-8'): 
            session['logged_in'] = api_list[0]['username'] 
            return redirect(url_for('index')) 
        return 'Invalid username/password!' 
      else: 
        flash("Invalid Authentication") 

    return 'Invalid User!' 

完成后,您的登录页面将显示在根 URL,并且应该是这样的:

正如您所看到的,我们提供了一个链接“立即注册”,以为新用户创建帐户。

请记住,我们正在使用 API 来从我们的数据库中的用户集合对用户进行身份验证。

注册用户

让我们继续创建我们的注册页面,以帮助注册新用户,以便他们也可以发推文。

让我们创建signup.html,它将要求用户提供详细信息。检查以下代码片段:

     <div class="container"> 
      <div class="row"> 
        <center><h2>Sign up</h2></center> 
          <div class="col-md-4 col-md-offset-4"> 
              <form method=POST action="{{ url_for('signup') }}"> 
                  <div class="form-group"> 
                      <label >Username</label> 
                      <input type="text" class="form-control"
                        name="username" placeholder="Username"> 
                  </div> 
                  <div class="form-group"> 
                      <label >Password</label> 
                      <input type="password" class="form-control" 
                      name="pass" placeholder="Password"> 
                  </div> 
                  <div class="form-group"> 
                      <label >Email</label> 
                      <input type="email" class="form-control" 
                     name="email" placeholder="email"> 
                  </div> 
                  <div class="form-group"> 
                      <label >Full Name</label> 
                      <input type="text" class="form-control" 
                      name="name" placeholder="name"> 
                  </div> 
                  <button type="submit" class="btn btn-primary btn-
                     block">Signup</button> 
               </form> 
               <br> 
            </div> 
          </div> 
      </div> 

上述代码基本上是需要后端 API 将数据提交给用户的模板。

app.py:
    @app.route('/signup', methods=['GET', 'POST']) 
    def signup(): 
      if request.method=='POST': 
        users = mongo.db.users 
        api_list=[] 
        existing_user = users.find({'$or':  
        [{"username":request.form['username']} ,
         {"email":request.form['email']}]}) 
            for i in existing_user: 
              api_list.append(str(i)) 
            if api_list == []: 
              users.insert({ 
              "email": request.form['email'], 
              "id": random.randint(1,1000), 
              "name": request.form['name'], 
              "password": bcrypt.hashpw(request.form['pass'].
                encode('utf-8'), bcrypt.gensalt()), 
              "username": request.form['username'] 
            }) 
            session['username'] = request.form['username'] 
            return redirect(url_for('home')) 

          return 'That user already exists' 
      else : 
        return render_template('signup.html') 

用户注册后,它将设置会话,并将其重定向到您的主页。

您的注册页面应该看起来像这样:

我们已经验证了用户,但如果他想要更新个人信息怎么办?让我们创建一个个人资料页面,以帮助他们这样做。

用户资料

让我们创建一个个人资料页面(profile.html),用户在主页登录后可以在导航面板中访问。

将以下代码添加到profile.html

     <div class="container"> 
      <div class="row"> 
        <center><h2>Profile</h2></center> 
          <div class="col-md-4 col-md-offset-4"> 
              <form method=POST action="{{ url_for('profile') }}"> 
                  <div class="form-group"> 
                      <label >Username</label> 
                      <input type="text" class="form-control"
                       name="username" value='{{username}}'> 
                  </div> 
                  <div class="form-group"> 
                      <label >Password</label> 
                      <input type="password" class="form-control"
                      name="pass" value='{{password}}'> 
                  </div> 
                  <div class="form-group"> 
                      <label >Email</label> 
                      <input type="email" class="form-control" 
                      name="email" value={{email}}> 
                  </div> 
                  <div class="form-group"> 
                      <label >Full Name</label> 
                      <input type="text" class="form-control" 
                      name="name" value={{name}}> 
                  </div> 
                  <button type="submit" class="btn btn-primary btn-
                   block">Update</button> 
                </form> 
              <br> 
           </div> 
       </div> 
     </div> 

由于我们已经创建了个人资料,我们需要为个人资料创建一个路由,它将读取数据库以获取用户详细信息,并将 POST 回数据库。

app.py:
    def profile(): 
       if request.method=='POST': 
         users = mongo.db.users 
         api_list=[] 
         existing_users = users.find({"username":session['username']}) 
         for i in existing_users: 
            api_list.append(str(i)) 
         user = {} 
         print (api_list) 
         if api_list != []: 
            print (request.form['email']) 
            user['email']=request.form['email'] 
            user['name']= request.form['name'] 
            user['password']=request.form['pass'] 
            users.update({'username':session['username']},{'$set':
          user} ) 
        else: 
            return 'User not found!' 
        return redirect(url_for('index')) 
      if request.method=='GET': 
        users = mongo.db.users 
        user=[] 
        print (session['username']) 
        existing_user = users.find({"username":session['username']}) 
        for i in existing_user: 
            user.append(i) 
        return render_template('profile.html', name=user[0]['name'], 
        username=user[0]['username'], password=user[0]['password'], 
        email=user[0]['email']) 

一旦添加了这最后一部分代码,您的个人资料页面应该看起来像这样:

此外,我们应该在导航模板的Tweet.js中添加个人资料链接,添加以下几行:

      <li><a href="/profile">Profile</a></li> 
      <li><a href="/logout">Logout</a></li> 

现在您的主页应该看起来像这样:

登出用户

app.py:
    @app.route("/logout") 
    def logout(): 
      session['logged_in'] = False 
      return redirect(url_for('home')) 

现在我们的应用程序已经完全构建起来,从用户登录,到提交他们的推文,然后退出登录。

测试 React webViews

由于我们正在构建 webViews,我们需要测试它们以在发生之前捕捉一些错误。此外,测试将帮助您构建更好的代码。

有许多 UI 测试框架可以帮助您测试 Web 应用程序。以下部分讨论了其中两个。

Jest

Jest 是一个单元测试框架,由 Facebook 提供用于测试 JavaScript。它用于测试单个组件。它简单、标准且独立。

它基于虚拟 DOM 实现测试组件,并运行不同的测试来检查功能。它会自动解决依赖关系。此外,您可以并行运行所有测试。

您可以参考以下链接,这可以帮助您为 React 应用编写测试用例:

facebook.github.io/jest/docs/tutorial-react.html

Selenium

Selenium 是一个开源的、可移植的自动化软件测试工具,用于测试 Web 应用程序。它提供端到端测试,这意味着它是针对真实浏览器执行测试场景来测试多层应用程序堆栈的过程。

它具有以下不同的组件:

  • IDE:这可以帮助您描述测试工作流程。

  • Selenium WebDriver:这可以自动化浏览器测试。它直接发送命令到浏览器并接收结果。

  • Selenium RC:这个远程控制器可以帮助您创建测试用例。

  • 网格:这在不同浏览器和并行运行测试用例。

这是您可以用来测试我们的 Web 应用程序的最佳工具之一,我强烈推荐使用。

您可以在www.seleniumhq.org/docs/了解更多关于 Selenium 的信息。

摘要

在本章中,我们的重点是创建前端用户 webViews 以及如何改进它们以吸引消费者。您还学会了 React 如何帮助我们构建这些 webViews 并实现与后端服务的交互。在接下来的章节中,事情将变得更有趣,因为我们将玩转我们的前端应用程序,并解释如何使用 Flux 来处理来自互联网的大量请求。

第六章:使用 Flux 创建可扩展的 UI

在上一章中,我们为我们的应用程序创建了 Web 视图,还看到了前端和后端应用程序之间的集成,这对理解是非常重要的。

在本章中,我们将专注于构建我们的前端。理想情况下,每个模块应该负责一件事。就像我们的主要组件一样,我们在单个模块中运行了太多操作。除了渲染不同的视图之外,我们还有代码来向端点发出 API 请求并接收、处理和格式化响应。

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

  • 理解 Flux

  • 在 React 上实现 Flux

理解 Flux

Flux是 Facebook 创建的一种模式,用于使用 React 构建一致和稳定的 Web 应用程序。React 并不给你管理数据的能力;相反,它只是通过 props 和组件接受数据,而组件进一步处理数据。

React 库并不真正告诉你如何获取组件,或者在哪里存储数据,这就是为什么它被称为视图层。在 React 中,我们没有像 Angular 或 Backbone 那样的框架。这就是 Flux 的用武之地。Flux 并不是一个真正的框架,而是一种模式,它将让你构建自己的视图。

什么是 Flux 模式?我们有你的 React 组件,比如 Tweet 组件等等,在 Flux 模式中,这些组件会做两件事--它们要么执行动作,要么监听存储器。在我们的用例中,如果用户想要发布推文,组件需要执行动作,然后动作与存储器交互,更新模式到 API,并将响应给组件。以下图表将让你更清楚地了解 Flux:

Flux 概念

在继续之前,以下是你需要了解的 Flux 概念:

  • 动作:这是组件与 API 端点交互并更新它们的方式。在我们的情况下,我们使用它发布新推文。动作将动作传输到调度器。它可能创建多个动作。

  • 调度器:这会分发每一个事件,并将其发送给每一个订阅者,基本上就是存储器。

  • 存储器:这是 Flux 的一个重要部分。组件总是监听存储器的任何更改。比如,如果你写了一条新推文,那就是一个动作,无论推文在存储器中更新到哪里,都会触发一个事件,并且组件会意识到它必须使用最新的数据进行更新。如果你来自 AngularJS 世界,存储器就是一个服务,或者如果你是 Backbone.js 的话,存储器只是一个集合。

  • 组件:这用于存储动作名称。

我们将使用JSX文件而不是JS,因为它们之间没有太大的区别--JS是标准的 Javascript,而JSX是一种类似 HTML 的语法,你可以在 React 中使用它来轻松而直观地创建 React 组件。

向 UI 添加日期

在我们深入研究 Flux 之前,我们需要向我们的视图添加一个小功能,即日期功能。之前,你看到的是存储在数据库中的推文的时间,格式为TZ;然而,理想情况下,它应该与当前时间进行比较,并应该以此为参考显示。

为了做到这一点,我们需要更新我们的main.jsx文件,以便它可以格式化我们的推文。将以下代码添加到main.jsx中:

    updatetweets(tweets){ 
        let updatelist = tweets.map(tweet => { 
         tweet.updatedate = moment(tweet.timestamp).fromNow(); 
         return tweet; 
       }); 
   }

我们的工作到此为止。现在,我们的推文应该看起来像这样:

使用 Flux 构建用户界面

在 Flux 中,我们将定义每个模块的责任,并且它也应该是单一的。React 的责任是在数据发生变化时重新渲染视图,这对我们来说是很好的。我们所需要做的就是使用类似 Flux 这样的东西来监听这些数据事件,它将管理我们的数据。

使用 Flux,你不仅分离了模块的责任,还可以在应用程序中实现单向流动,这就是为什么 Flux 如此受欢迎。

在 Flux 循环中,对于每个模块,总是有一个方向要遵循。这种对流程的有意约束是使 Flux 应用程序易于设计、易于增长、易于管理和维护的原因。

以下图表将让您更清楚地了解 Flux 架构:

对于图表,我参考了 Flux 存储库(github.com/facebook/flux)。

Actions 和 dispatcher

要开始使用 Flux,我们必须选择一个起点。可以是任何东西。我发现从 actions 开始是个不错的选择。您还必须选择一个流向。您可以顺时针或逆时针。顺时针对您来说可能是一个不错的起点,所以我们将这样做。

不要忘记使用以下命令直接安装 Flux 库:

$ npm install flux --save

请注意,上述命令应该从我们的应用程序目录中执行,或者您可以将其添加到package.json中,并执行npm install来安装包。

现在,让我们从 action 作为起点开始,我们将遵循单一职责原则。我们将创建一个 actions 库来与 API 通信,并创建另一个 action 来与 dispatcher 通信。

让我们从静态目录中创建actions文件夹开始。我们将在这个目录中保存所有的 actions。

由于我们有两个需要执行的 actions--可能是列出 tweets 或添加新 tweets--我们将从列出 tweets 开始。创建一个Tactions文件,其中包含getAllTweets函数,该函数应该调用 REST API 来获取所有的 tweets,如下所示:

   export default{ 
    getAllTweets(){ 
    //API calls to get tweets. 
    } 
   } 

我提到过基于 Flux 的应用程序易于设计,对吧?这就是原因。因为我们知道这个 actions 模块具有单一职责和单一流程--要么我们在这里提供 API 调用,要么最好调用一个模块来为应用程序进行所有 API 调用。

更新Tactions.jsx文件如下:

    import API from "../API" 
     export default{ 
      getAllTweets(){ 
       console.log(1, "Tactions for tweets"); 
        API.getAllTweets(); 
      }, 
    } 

如您所见,我们导入了 API 模块,它将调用 API 来获取 tweets。

因此,让我们在静态目录中创建API.jsx,其中包含以下代码片段来从后端服务器获取 tweets:

    export default{ 
      getAllTweets(){ 
       console.log(2, "API get tweets"); 
       $.getJSON('/api/v2/tweets', function(tweetModels) { 
          var t = tweetModels 
        // We need to push the tweets to Server actions to dispatch 
        further to stores. 
       }); 
      } 

在 actions 目录中创建Sactions文件,它将调用 dispatcher 并定义actionType

    export default{ 
      receivedTweets(rawTweets){ 
       console.log(3, "received tweets"); 
      //define dispatcher.     
     } 
   } 

如您所见,我们仍然需要定义 dispatcher。幸运的是,Facebook 创建了一个随 Flux 包一起提供的 dispatcher。

如前所述,Dispatcher是您的应用程序的中央枢纽,它分发Actions和注册回调的数据。您可以参考以下图表更好地理解数据流:

创建一个名为dispatcher.jsx的新文件,其中将使用以下代码创建一个 dispatcher 的实例:

    import Flux from 'flux'; 

    export default new Flux.Dispatcher();   

就是这样。现在您可以在应用程序的任何地方导入这个 dispatcher。

因此,让我们更新我们的Sactions.jsx文件,其中您将找到receivedTweets函数,如下所示的代码片段:

    import AppDispatcher from '../dispatcher'; 
    receivedTweets(rawTweets){ 
      console.log(3, "received tweets"); 
      AppDispatcher.dispatch({ 
        actionType: "RECEIVED_TWEETS", 
         rawTweets 
      }) 
     } 

receivedTweets函数中,有三件事需要描述。首先,rawTweets将从API.jsx中的getAllTweets函数接收,我们需要按照以下方式进行更新:

   import SActions from './actions/SActions'; 

   getAllTweets(){ 
     console.log(2, "API get tweets"); 
     $.getJSON('/api/v2/tweets', function(tweetModels) { 
        var t = tweetModels 
        SActions.receivedTweets(t) 
    }); 

Stores

Stores 通过控制应用程序内的数据来管理应用程序状态,这意味着 stores 管理数据、数据检索方法、dispatcher 回调等。

为了更好地理解,请参考以下图表:

现在我们已经定义了我们的 dispatcher,接下来,我们需要确定订阅者对 dispatcher 提供的更改。

在静态目录中的 stores 中创建一个单独的目录,其中将包含所有的 store 定义。

让我们创建一个TStore文件,它将订阅 dispatcher 发出的任何更改。将以下代码添加到TStore文件中:

    import AppDispatcher from "../dispatcher"; 

    AppDispatcher.register(action =>{ 
     switch (action.actionType) { 
     Case "RECEIVED_TWEETS" : 
    console.log(4, "Tstore for tweets"); 
     break; 
      default: 
    } 
  }); 

在这一点上,我们已经开始了推文操作,向 API 模块发送消息以获取所有推文。API 执行了这一操作,然后调用服务器操作将数据传递给调度程序。然后,调度程序标记了数据并将其分发。我们还创建了基本上管理数据并从调度程序请求数据的存储。

目前,您的存储尚未与我们的应用程序连接。存储应该在发生更改时发出更改,并且基于此,视图也将发生更改。

因此,我们的主要组件对存储发出的更改事件感兴趣。现在,让我们导入我们的存储。

在我们继续之前,让我们看看我们的应用程序的完整流程是否正常工作。应该是这样的:

在达到应用程序创建的一定稳定状态后,继续检查用户界面是一个很好的做法。

让我们继续。目前,我们只是分发推文,接下来,我们需要决定如何处理这些推文。因此,让我们首先接收推文,然后相应地向视图发出更改。我们将使用发射器来做到这一点。

Emitter是我们之前使用npm安装的事件库的一部分。因此,我们可以从那里导入它。请注意,它不是默认导出,而是它的解构属性。然后,我们的存储将是此推文EventEmitter类的实例。

让我们按照以下方式更新我们的TStore.jsx文件:

    import { EventEmitter } from "events"; 

    let _tweets = [] 
     const CHANGE_EVENT = "CHANGE"; 

     class TweetEventEmitter extends EventEmitter{ 
     getAll(){ 
       let updatelist = _tweets.map(tweet => { 
          tweet.updatedate = moment(tweet.timestamp).fromNow(); 
         return tweet; 
        }); 
     return _tweets; 
     } 
     emitChange(){ 
       this.emit(CHANGE_EVENT); 
     } 

     addChangeListener(callback){ 
      this.on(CHANGE_EVENT, callback); 
     } 
     removeChangeListener(callback){ 
       this.removeListener(CHANGE_EVENT, callback); 
    } 
   } 
   let TStore = new TweetEventEmitter(); 

   AppDispatcher.register(action =>{ 
    switch (action.actionType) { 
      case ActionTypes.RECEIVED_TWEETS: 
        console.log(4, "Tstore for tweets"); 
        _tweets = action.rawTweets; 
        TStore.emitChange(); 
      break; 
     } 
     }); 
    export default TStore; 

哇,一次理解这么多代码!让我们一部分一部分地理解它,以及代码的流程。

首先,我们将使用以下导入实用程序从事件包中导入EventEmitter库:

   import { EventEmitter } from "events"; 

接下来,我们将在_tweets中存储接收到的推文,并更新getAll()函数中的推文,以便在视图中显示推文的时间与当前系统时间的参考:

   getAll(){ 
     let updatelist = _tweets.map(tweet => { 
         tweet.updatedate = moment(tweet.timestamp).fromNow(); 
         return tweet; 
       }); 
     return _tweets; 
   }

我们还为视图创建了添加和删除更改事件侦听器的函数。这两个函数也只是围绕EventEmitter语法的包装。

这些函数接受由视图发送的callback参数。这些函数基本上是为了向视图添加或删除侦听器,以便开始或停止监听存储中的这些更改。将以下代码添加到TStore.jsx中以执行此操作:


    addChangeListener(callback){ 
      this.on(CHANGE_EVENT, callback); 
    } 
    removeChangeListener(callback){ 
     this.removeListener(CHANGE_EVENT, callback); 
    } 

确保在控制台中没有任何更新后的代码错误。

让我们继续前进,即在主要组件中创建一个函数,从存储中提取数据并为组件的状态准备一个对象。

让我们在main.jsx中编写getAppState()函数,该函数维护应用程序的状态,如下所示:

    let getAppState = () =>{ 
      return { tweetslist: TStore.getAll()}; 
    } 

如前所述,文件扩展名实际上并不重要,无论是.js还是.jsx

现在,我们将从Main类中调用此函数,并且还将调用我们在main.jsx中创建的添加和删除侦听器函数,使用以下代码块:

   import TStore from "./stores/TStore"; 

   class Main extends React.Component{ 
     constructor(props){ 
      super(props); 
      this.state= getAppState(); 
      this._onChange = this._onChange.bind(this); 
      //defining the state of component. 
     } 
   // function to pull tweets 
     componentDidMount() { 
     TStore.addChangeListener(this._onChange); 
    } 
   componentWillUnMount() { 
     TStore.removeChangeListener(this._onChange); 
    } 

   _onChange(){ 
    this.setState(getAppState()); 
    } 

此外,我们必须更新render函数以获取Tweetslist状态以在视图中显示,可以使用以下代码片段完成:

    render(){ 
      return ( 
       <div> 
       <Tweet sendTweet={this.addTweet.bind(this)}/> 
          <TweetList tweet={this.state.tweetslist}/> 
       </div> 
       ); 
      } 

很棒,我们现在已经做了几乎所有的事情;我们的推文应该可以正常显示,如下所示:

太棒了!我们的应用程序运行正常。

如果您查看 Flux 的架构图,我们已经完成了 Flux 的流程一次,但我们仍然需要通过创建 API 来完成循环,以添加新推文。

让我们通过使用 Flux 发送新推文功能来实现它。我们将在main.jsx中进行一些更改。在render函数中,将Tweetcall更改为以下行的addTweet函数:

    <Tweet sendTweet={this.addTweet.bind(this)}/> 

而不是使用参数调用Tweet组件,如下所示:

    <Tweet /> 

此外,在Tweet组件中,我们将调用TActions模块来添加新推文。更新Tweet组件中的代码如下:

    import TActions from "../actions/Tactions" 
    export default class Tweet extends React.Component { 
     sendTweet(event){ 
      event.preventDefault(); 
      // this.props.sendTweet(this.refs.tweetTextArea.value); 
      TActions.sendTweet(this.refs.tweetTextArea.value); 
      this.refs.tweetTextArea.value = ''; 
     } 

    } 

Tweet组件中的Render函数保持不变。

让我们添加一个新的 sendTweet 函数,它将调用后端应用程序的端点 URL 进行 API 调用,并将其添加到后端数据库。

现在,我们的 Taction.jsx 文件应该是这样的:

   import API from "../API" 

  export default{ 
    getAllTweets(){ 
     console.log(1, "Tactions for tweets"); 
     API.getAllTweets(); 
    }, 
    sendTweet(body){ 
      API.addTweet(body); 
     } 
   } 

现在,在 API.jsx 中添加 API.addTweet 函数,它将进行 API 调用,并且还会更新推文列表的状态。将以下 addTweet 函数添加到 API.jsx 文件中:

   addTweet(body){ 
      $.ajax({ 
          url: '/api/v2/tweets', 
          contentType: 'application/json', 
          type: 'POST', 
          data: JSON.stringify({ 
         'username': "Pardisturn", 
         'body': body, 
          }), 
       success: function() { 
            rawTweet => SActions.receivedTweet({ tweetedby:
            "Pardisturn",body: tweet, timestamp: Date.now}) 
        }, 
        error: function() { 
               return console.log("Failed"); 
         } 
      }); 
     } 

此外,我们正在将新添加的推文传递给服务器操作,以便将它们分派并可用于存储。

让我们添加一个新的函数 receivedTweet,它将分派它们。使用以下代码片段来实现:

    receivedTweet(rawTweet){ 
      AppDispatcher.dispatch({ 
        actionType: ActionTypes.RECEIVED_TWEET, 
        rawTweet 
       }) 
     } 

ActionTypes 经常在静态目录的 constants.jsx 中定义。

现在,让我们在推文存储中定义 RECEIVED_TWEETactiontype case,以便发出更改,以便视图进一步采取行动。以下是在 TStore.jsx 中定义的更新的 Appdispatcher.register 函数:

   AppDispatcher.register(action =>{ 
    switch (action.actionType) { 
         case ActionTypes.RECEIVED_TWEETS: 
         console.log(4, "Tstore for tweets"); 
         _tweets = action.rawTweets; 
         TStore.emitChange(); 
          break; 
        case ActionTypes.RECEIVED_TWEET: 
          _tweets.unshift(action.rawTweet); 
          TStore.emitChange(); 
          break; 
       default: 

      } 
    }); 

现在,我们基本上已经完成了使用 Flux 添加新的推文模块,它应该完全正常工作,如下面的截图所示:

现在,如果我们点击“立即推文”按钮,推文应该被添加,并且应该在下面的面板中显示,如下所示:

摘要

在本章中,您学习了如何使用 Flux 模式来构建我们的应用程序,并且我们也了解了 Flux 的不同概念,比如分发器、存储等。Flux 为您提供了良好的模式来在模块之间分配责任,这确实需要被理解,因为我们正在为云平台开发应用程序,比如 AWS、Azure 等,所以我们的应用程序应该具有高度的响应性。这就是我们从构建用户界面方面所拥有的一切,但在接下来的章节中,我们将了解一些重要的概念,比如事件溯源,以及如何通过使用不同的身份验证方法使应用程序更加安全。

第七章:学习事件溯源和 CQRS

在上一章中,我们看了看当前业务模型的缺点,现在,在本章中,我们将看看事件溯源(ES)和命令查询责任分离(CQRS)如何有助于克服这些问题。

在本章中,我们将讨论一些处理大规模可扩展性的架构设计。我们还将研究事件溯源和 CQRS 这两种模式,这些模式都是为了解决如此大量请求的问题响应行为。

我们许多人认为遵守十二要素应用程序将使我们的应用程序成为具有更高可扩展性的云原生应用程序,但是还有其他策略,比如 ES 和 CQRS,可以使我们的应用程序更可靠。

由于云原生应用程序面向互联网,我们期望来自不同来源的成千上万甚至数百万的请求。实施基础架构架构来处理请求的扩展或缩小是不够的。您需要使您的应用程序支持如此巨大的扩展。这就是这些模式出现的时候。

本章将涵盖的主题如下:

  • 事件溯源介绍

  • 介绍命令查询责任分离

  • 实现 ES 和 CQRS 的示例代码

  • 使用 Apache Kafka 进行事件溯源

介绍

让我们从审查n层架构开始,其中我们有一些客户端、网络、业务模型、一些业务逻辑、一些数据存储等等。这是一个基本模型,您会发现它作为任何架构设计的一部分。它看起来像下面的图表:

正如您在这个架构中所看到的,我们有这些不同的模型在起作用:

  • 视图模型:这基本上是为客户端交互而设计的

  • DTO 模型:这是客户端和 REST 端点之间的通信

  • 业务模型:这是 DAO(数据访问对象)和业务服务的组合,解释用户请求,并与存储服务通信

  • E-R 模型:这定义了实体之间的关系(即 DTO 和 RDMS/NDMS)

现在您对架构有了一些了解,让我们了解其特点,如下所示:

  • 应用程序的相同堆栈:在这个模型中,我们对所有读写操作使用相同的元素堆栈,从 REST API 到业务服务,然后访问存储服务等等,因为所有不同的组件代码都作为单个实体一起部署。

以下图表显示了通过不同模型的读/写操作流程:

  • 相同的数据模型:在这种情况下,您会发现大多数情况下,我们用于业务逻辑处理或读写数据的数据模型相同或类似。

  • 部署单元:我们使用粗粒度的部署单元,包括以下内容:

  • 一个构建(一组可执行的组件)

  • 文档(最终用户支持材料和发布说明)

  • 安装工件,将读取和写入代码结合在一起

  • 直接访问数据:如果我们想要更改数据,通常我们会继续。特别是在关系型数据库的情况下,我们直接更改数据,如下例--如果我们想要使用另一个数据集更新用户 ID 1的行,我们通常会直接这样做。而且,一旦我们更新了这个值,旧值将从应用程序以及存储端无效,并且无法检索!

到目前为止,我们一直在使用前面的方法,并且我会说,就用户请求的响应而言,它在很大程度上是经过验证和成功的。然而,与之相比,还有其他替代方法可以表现得更好。

让我们讨论上述业务架构方法的缺点,如下所示:

  • 无法独立扩展:由于我们的读写操作代码驻留在同一位置,我们无法独立扩展应用程序的读取或写入。假设在特定时间点,应用程序的读取占 90%,写入占 10%,我们无法独立扩展读取。为了扩展读取,我们需要扩展整个架构,这是没有用的,还会增加资源的浪费。

  • 没有数据历史:由于我们处理的是直接更新数据的情况,一旦数据更新,应用程序将在一段时间后开始显示最新的数据集。此外,一旦数据集更新,旧的数据值就不会被跟踪,因此会丢失。即使我们想要实现这种功能,我们也需要编写大量的代码来启用它。

  • 单片式方法:这种方法往往是一种单片式方法,因为我们试图将事物合并在一起。此外,我们有粗粒度的部署单元,并且我们试图将不同组件的代码放在一起。因此,这种方法最终会导致一团糟,很难解决。

解决这些挑战的一种方法是事件溯源。

理解事件溯源

简单来说,事件溯源是一种架构模式,它通过一系列事件来确定应用程序的状态。

理解事件溯源的最佳方法是使用类比。其中一个最好的例子就是在线购物,这是一个事件处理系统。有人下订单,订单被注册到供应商订购系统的订单队列中。然后,订单在不同阶段被通知给客户。

所有这些事件一个接一个地发生,形成了一个称为事件流的事件序列,应该看起来像以下图表所示:

因此,事件溯源考虑了过去发生的事件,并记录了基于某些交易进行处理。

理想的事件溯源系统是基于以下图表中显示的构建模块的:

前面的图表描述了一个理想的事件处理系统,从应用程序开始到创建与某个事件相关的事件,然后将它们放入事件队列进行进一步处理,由事件处理程序执行。根据事件的描述,事件处理程序相应地处理它们,并将它们注册到存储中。

事件溯源遵循某些法则/原则,这使得应用程序开发成为一个有结构和纪律的过程。大多数人通常觉得事件溯源很难,或者他们认为它是绝对的,因为这些原则不能被打破,否则会在应用程序中造成巨大的混乱。

事件溯源的法则

以下是一些事件溯源法则,需要在任何系统(即应用程序设计)中实施时保持:

  • 幂等性:理想的事件溯源业务逻辑必须是幂等的。这意味着当您针对一系列数据执行业务逻辑时,应用程序的结果状态将始终保持不变。是的,无论您执行业务逻辑的次数如何,它的结果状态都将保持不变。

  • 隔离:事件溯源不应依赖外部事件流。这是事件溯源的最重要原则之一。通常,业务逻辑很少在真空中执行。应用程序通常与外部实体进行交互以进行参考。此外,应用程序使用来自外部来源的缓存信息,即使开发人员没有考虑到这一点。现在,出现的问题是,如果您的业务逻辑使用外部输入来计算结果会发生什么?让我们以股票交易为例,股票价格不断变化,这意味着在状态计算时的股价在多次评估中不会相同,这违反了幂等规则。

根据开发人员的理解,这是一个非常难以满足的条件。然而,处理这个问题的解决方案是从外部事件向主事件流中注入通知。由于这些通知现在是主事件流的一部分,您将每次都得到预期的结果。

  • 质量保证:一个经过完全开发的事件溯源应用程序应该是一个经过充分测试的应用程序。为事件溯源应用程序编写测试用例很容易--通常需要一系列输入并返回一些状态,考虑到您是按照先前定义的原则编写测试用例。

  • 可恢复性:事件溯源应用程序应支持恢复和重放。如果您有一个符合十二要素应用程序所有指南的云原生应用程序,以创建适合云平台的应用程序,事件溯源在灾难恢复中发挥着重要作用。

假设事件流是持久的,事件溯源应用程序的初始优势是计算应用程序的状态。通常,在云环境中,由于多种原因可能导致应用程序崩溃;事件溯源可以帮助我们识别应用程序的最后状态,并快速恢复以减少停机时间。此外,事件溯源的重放功能使您能够在审计和故障排除时查看过去的状态。

  • 大数据:事件溯源应用程序通常会生成大量数据。由于事件溯源应用程序跟踪每个事件,可能会生成大量数据。这取决于您有多少事件,它们多频繁到达,以及事件的数据负载有多大。

  • 一致性:事件溯源应用程序通常会保持事件注册的一致性。想想银行交易--银行交易期间发生的每个事件都非常重要。应该注意,在记录时应保持一致性。

非常重要的是要理解这些事件是过去发生的事情,因为当我们命名这些事件时,它们应该是可以理解的。一些有效的事件名称示例可能如下:

  • PackageDeliveredEvent

  • UserVerifiedEvent

  • PaymentVerifiedEvent

无效的事件将被命名如下:

  • CreateUserEvent

  • AddtoCartEvent

以下是一个事件的示例代码:

    class ExampleApplication(ApplicationWithPersistencePolicies): 
      def __init__(self, **kwargs): 
        super(ExampleApplication, self).__init__(**kwargs) 
       self.snapshot_strategy = None 
       if self.snapshot_event_store: 
           self.snapshot_strategy = EventSourcedStrategy( 
               event_store=self.snapshot_event_store, 
           ) 
       assert self.integer_sequenced_event_store is not None 
       self.example_repository = ExampleRepository( 
           event_store=self.integer_sequenced_event_store, 
           snapshot_strategy=self.snapshot_strategy, 
       ) 

有一些要注意的要点:

  • 每个事件都是不可变的,这意味着一旦触发了事件,就无法撤销。

  • 您永远不会删除事件。即使我们试图删除事件,我们也将删除视为一个事件。

  • 事件流由消息代理架构驱动。一些消息代理包括 RabbitMQ、ActiveMQ 等。

现在,让我们讨论事件溯源的一些优点,如下所示:

  • 事件溯源能够快速重建系统

  • 事件溯源使您对数据具有控制权,这意味着我们需要的处理数据可以通过查看事件流轻松获取,比如审计、分析等

  • 通过查看事件,很容易理解在一段时间内发生了什么错误,考虑到一组数据

  • 事件重放在故障排除或错误修复期间会有优势

现在,问题出现了,由于我们生成了如此大量的事件,这是否会影响应用程序的性能?我会说,是的!

由于我们的应用程序为每个需要由事件处理程序处理的事务生成事件,因此应用程序的响应时间得到了缩短。解决这个问题的方法是 CQRS。

CQRS 简介

命令查询职责分离是一个花哨的模式名称,意味着解耦系统的输入和输出。在 CQRS 中,我们主要讨论应用程序的读和写特性;因此,在 CQRS 的上下文中,命令主要是写操作,而查询是读操作,责任意味着我们分离了读和写操作。

如果我们看一下第一部分介绍中描述的架构,并应用 CQRS,那么架构将被分成两半,看起来会是这样的:

现在我们将看一些代码示例。

传统的接口模块会是这样的:

    Class managementservice(interface): 
     Saveuser(userdata); 
    Updateuser(userid); 
    listuserbyusername(username); 
    listuserbyid(userid); 

分离,或者我更喜欢称之为 CQRS-化的接口,会是这样的:

    Class managementcommandservice(interface): 
      Saveuser(userdata); 
    Updateuser(userid); 
    Class managementqueryservice(interface): 
    listuserbyusername(username); 
    listuserbyid(userid); 

因此,在实施 CQRS 和事件溯源后,整体架构会像下图所示的那样:

这是在实施事件溯源和 CQRS 后的完整架构。

在经典的单体应用中,您有写入数据库的端点和从中读取的端点。相同的数据库用于读取和写入操作,并且在从数据库接收到确认或提交之前,不会回复端点。

在大规模情况下,具有高入站事件吞吐量和复杂事件处理要求,您不能承受读取慢查询,也不能每次获得新的入站事件时等待处理。

读和写操作的流程如下:

  • 写模型:在这种情况下,当从端点触发命令并在命令业务服务接收到时,首先为每个事件发出事件到事件存储。在事件存储中,您还有一个命令处理器,或者换句话说,事件处理程序,这个命令处理器能够将应用程序状态派生到一个单独的存储中,这可能是一个关系型存储。

  • 读模型:在模型的情况下,我们只需使用查询端点来查询客户端想要读取或检索的数据,以供应用程序使用。

最大的优势是我们不需要通过模型(在前图的右侧)进行。在查询数据库时,这个过程使我们的查询执行更快,并减少了响应时间,从而提高了应用程序的性能。

CQRS-化架构的优势

这种架构有以下优点:

  • 独立的可伸缩性和部署:现在我们可以根据其使用情况扩展和部署单个组件。就像微服务的情况一样,我们现在可以为每个任务拥有单独的微服务,比如一个读微服务和一个写微服务,在这个架构堆栈中。

  • 技术选择:在业务模型的不同部分选择技术的自由。例如,对于命令功能,我们可以选择 Scala 或类似的语言(假设我们有一个复杂的业务模型,并且有大量数据要写入)。在查询的情况下,我们可以选择,例如,ROR(Ruby on Rails)或 Python(我们已经在使用)。

这种类型的架构最适合于DDD(领域驱动设计)的有界上下文,因为我们可以为微服务定义业务上下文。

与 ES 和 CQRS 相关的挑战

每种架构设计模型在实施时都有自己的挑战。让我们讨论 ES 和 CQRS 的挑战:

  • 不一致性:使用 ES 和 CQRS 开发的系统大多是一致的。然而,由于我们在事件存储中存储命令业务服务发出的事件,并且在主存储中也存储应用程序的状态,我会说这种系统并不完全一致。如果我们真的想使用 ES 和 CQRS 使我们的系统完全一致,我们需要将我们的事件存储和主存储放在一个单一的关系数据库上,我们的命令处理器应该处理所有我们的传入事件,并同时将它们存储在两个存储中,如下图所示:

我认为一致性水平应该由对业务领域的理解来定义。需要了解事件中需要多少一致性,以及这些一致性会带来多大的成本。在检查业务领域之后,您将能够考虑上述因素做出这些决定。

  • 验证:当我们谈论验证客户注册表单时,这非常容易,我们需要验证各个字段等等。但实际的验证是在我们需要基于唯一性进行验证时--比如说我们有一个具有特定用户凭据(用户名/密码)的客户。因此,确保用户名是唯一的是一个关键的验证,当我们有超过 200 万需要注册的客户时。在验证方面需要问一些问题,如下所示:

  • 验证的数据需求是什么?

  • 从哪里检索验证数据?

  • 验证的概率是多少?

  • 验证失败对业务的影响是什么?

  • 并行数据更新:这在数据一致性方面非常重要。比如说,您有一个用户想要在同一时间或在纳秒的差距内更新某些记录。在这种情况下,一致性和验证检查的可能性是具有挑战性的,因为有可能一个用户可能会覆盖另一个用户的信息,这可能会造成混乱。

克服挑战

在事件源中解决这样的问题的一种方法是在事件中添加版本,这将作为对数据进行更改的处理,并确保它得到充分验证的处理。

问题解决

让我们以以下图表中显示的用例为例,以了解在编写代码时如何理解事件源和 CQRS:

解释问题

在这种情况下,我们提供了用户详细信息,如用户 ID(应该是唯一的),用户名密码电子邮件 ID等等,我们需要创建两个要触发的写命令--UserRegistrationCommandUpdatePasswordCommand,触发两个事件UserRegisterEventsUpdatePasswordEvents。这个想法是,一旦注册用户,就应该能够根据他们的需求重置密码。

解决方案

为了解决这个问题,我们需要编写与写命令相关的函数来接收输入并更新事件存储。

现在,让我们将以下代码添加到commands.py文件中,其中将包含需要执行的写命令相关的代码:

   class userregister(object): 
     def __init__(self, user_id, user_name, password, emailid): 
       self.user_id = user_id 
       self.user_name = user_name 
       self.password = password 
       self.emailid = emaild 

   class updatepassword(object): 
     def __init__(self, user_id, new_password, original_version): 
       self.item_id = item_id 
       self.new_password = new__password 
       self.original_version = original_version 

因此,我们已经添加了与命令相关的函数,但它应该从某个地方调用用户详细信息。

让我们添加一个名为main.py的新文件,从这里将调用前面命令的函数。

在下面的代码中,我们通过触发事件来调用前面的代码:

    from aggregate import Aggregate 
    from errors import InvalidOperationError 
    from events import * 

   class userdetails(Aggregate): 
     def __init__(self, id = None, name = '"", password = "", emailid =
     "" ): 
       Aggregate.__init__(self) 
       self._apply_changes(Userdetails(id, name, password, emailid)) 

   def userRegister(self, userdetails): 
       userdetails = {1, "robin99", "xxxxxx", "robinatkevin@gmail.com" 
   } 
       self._apply_changes(UserRegisterevent(userdetails)) 

   def updatePassword(self, count):        
      password = "" 
       self._apply_changes(UserPasswordEvent(password)) 

让我们逐个理解前面的代码:

    def __init__(self, id = None, name = '"", password = "", emailid =
     "" ): 
       Aggregate.__init__(self) 
       self._apply_changes(Userdetails(id, name, password, emailid)) 

最后的代码初始化了self对象的一些默认值;这类似于任何编程语言中的初始化函数。

接下来,我们定义了userRegister函数,基本上收集userdetails,然后创建事件(UserRegisterevent(userdetails)))如下:

    def userRegister(self, userdetails): 
       userdetails = {1, "robin99", "xxxxxx", "robinatkevin@gmail.com"
    } 
       self._apply_changes(UserRegisterevent(userdetails)) 

因此,一旦用户注册,他/她就有权更新配置文件详细信息,这可能是电子邮件 ID、密码、用户名等--在我们的情况下,是密码。请参考以下代码:

     def updatePassword(self, count):        
      password = "" 
     self._apply_changes(UserPasswordEvent(password))

您可以编写类似的代码来更新电子邮件 ID、用户名或其他信息。

接下来,我们需要添加错误处理,因为在我们的main.py文件中,我们调用一个自定义模块errors来处理与操作相关的错误。让我们将以下代码添加到errors.py中以传递捕获的错误:

    class InvalidOperationError(RuntimeError): 
     pass 

正如您在main.py中所看到的,我们调用Aggregate模块,您一定想知道为什么要使用它。Aggregate模块非常重要,因为它跟踪需要应用的更改。换句话说,它强制事件将其所有未注释的更改提交到事件存储。

为了做到这一点,让我们将以下代码添加到一个名为aggregate.py的新文件中:

   class Aggregate(object): 
     def __init__(self): 
       self.uncommitted_changes = [] 

     @classmethod 
     def from_events(cls, events): 
       aggregate = cls() 
       for event in events: event.apply_changes(aggregate) 
       aggregate.uncommitted_changes = [] 
       return aggregate 

    def changes_committed(self): 
       self.uncommitted_changes = [] 

    def _apply_changes(self, event): 
       self.uncommitted_changes.append(event) 
       event.apply_changes(self) 

aggregate.py中,我们初始化了self对象,该对象在main.py中被调用,然后跟踪被触发的事件。一段时间后,我们将调用main.py中的更改来更新eventstore的更新值和事件。

events.py:
   class UserRegisterEvent(object): 
    def apply_changes(self, userdetails): 
       id = userdetails.id 
       name = userdetails.name 
       password = userdetails.password 
       emailid = userdetails.emailid 

   class UserPasswordEvent(object): 
    def __init__(self, password): 
       self.password = password 

    def apply_changes(password): 
       user.password = password 

现在我们还剩下命令处理程序,这非常重要,因为它决定了需要执行的操作以及需要触发的相应事件。让我们添加名为command_handler.py的文件,并添加以下代码:

    from commands import * 

    class UserCommandsHandler(object): 
     def __init__(self, user_repository): 
       self.user_repository = user_repository 

     def handle(self, command): 
       if command.__class__ == UserRegisterEvent: 
           self.user_repository.save(commands.userRegister(command.id, 
     command.name, command.password, command.emailid)) 
       if command.__class__ == UpdatePasswordEvent: 
           with self._user_(command.password, command.original_version)
      as item: 
               user.update(command.password) 
   @contextmanager 
     def _user(self, id, user_version): 
       user = self.user_repository.find_by_id(id) 
       yield user 
       self.user.save(password, user_version) 

command_handler.py中,我们编写了一个处理函数,它将决定事件执行流程。

正如您所看到的,我们调用了@contextmanager模块,在这里非常重要。

让我们来看一个场景:假设有两个人,Bob 和 Alice,两者都使用相同的用户凭据。假设他们都试图同时更新配置文件详细信息字段,例如密码。现在,我们需要了解这些命令是如何请求的。简而言之,谁的请求会先到达事件存储。此外,如果两个用户都更新密码,那么很可能一个用户的更新密码将被另一个用户覆盖。

解决问题的一种方法是在用户模式中使用版本,就像我们在上下文管理器中使用的那样。我们将user_version作为参数,它将确定用户数据的状态,一旦修改,我们可以增加版本以使数据一致。

因此,在我们的情况下,如果 Bob 的修改值首先更新(当然,使用新版本),如果 Alice 的请求版本字段与数据库中的版本不匹配,则 Alice 的更新请求将被拒绝。

一旦更新完成,我们应该能够注册和更新密码。虽然这只是一个示例,展示了如何实现 CQRS,但您可以扩展它以在其上创建微服务。

Kafka 作为事件存储

尽管我们已经看到了 CQRS 的实现,但我仍然觉得您可能对eventstore及其工作方式有一些疑问。这就是为什么我将采用 Kafka 的用例,它可以用作应用程序的eventstore

Kafka 通常是一个消息代理或消息队列(类似于 RabbitMQ、JMS 等)。

根据 Kafka 文档,事件溯源是一种应用设计风格,其中状态更改被记录为时间顺序的记录序列。Kafka 对非常大的存储日志数据的支持使其成为构建此风格的应用程序的优秀后端。

有关实施 Kafka 的更多信息,请阅读此链接上的文档:kafka.apache.org/documentation/

Kafka 具有以下基本组件:

  • 生产者:将消息发送到 Kafka

  • 消费者:这些订阅 Kafka 中的消息流

Kafka 的工作方式如下:

  • 生产者在 Kafka 主题中写入消息,这些消息可能是用户

  • 在 Kafka 主题中的每条消息都会被追加到分区的末尾

Kafka 只支持操作。

  • 分区代表事件流,主题可以被分类为多个主题

  • 主题中的分区彼此独立。

  • 为了避免灾难,Kafka 分区会被复制到多台机器上

  • 为了消费 Kafka 消息,客户端按顺序读取消息,从在 Kafka 中由消费者设置的偏移开始

使用 Kafka 应用事件溯源

让我们来看一个使用案例,客户端尝试执行某个操作,我们使用 Kafka 作为事件存储来捕获所有传递的消息。在这种情况下,我们有用户管理服务,它可能是负责管理所有用户请求的微服务。我们将从基于用户事件的 Kafka 主题开始识别主题,可能是以下之一:

  • UserCreatedEvent

  • UserUpdatedEvent

  • UserDeletionEvent

  • UserLoggedinEvent

  • UserRoleUpdatedEvent

这些事件理想情况下将由用户管理服务发布,并且所有微服务都将消费这些事件。以下图表显示了用户请求流程:

工作原理

用户向 API 网关发出POST请求,这是用户管理服务注册用户的入口点。API 网关反过来调用管理服务中的createUser方法。createUser端点对用户输入进行一系列验证。如果输入无效,它将抛出异常,并将错误返回给 API 网关。一旦用户输入被验证,用户将被注册,并且将触发UserCreatedEvent以在 Kafka 中发布。在 Kafka 中,分区捕获事件。在我们的示例中,用户主题有三个分区,因此事件将根据一些定义的逻辑发布到三个分区中的一个;这个逻辑由我们定义,根据用例的不同而变化。

所有读取操作,比如列出用户等,都可以直接从 readStore(如 PostgreSQL 等数据库)中检索出来。

总结

这是一个复杂的章节,但如果你完全理解了它,它将使你的应用程序高效且性能卓越。

我们首先了解了经典架构的缺点,然后讨论了 ES 和 CQRS 的概念和实现。我们还看了一个示例问题的实现。我们谈到了为什么这些模式有用,以及它们如何与大规模、云原生应用程序特别协调。

在接下来的章节中,我们将深入探讨应用程序的安全性。敬请关注!

第八章:保护网络应用程序

在本章中,我们将主要讨论如何保护您的应用程序免受可能导致数据丢失的外部威胁,从而影响整体业务。

网络应用程序安全始终是任何业务单位关注的问题。因此,我们不仅关注传统的应用程序逻辑和与数据相关的安全问题,还关注协议和平台方面的问题。开发人员变得更加负责,确保遵守有关网络应用程序安全的最佳实践。

记住这一点,本书旨在面向应用程序开发人员、系统管理员以及希望保持其应用程序安全的 DevOps 专业人员,无论是在应用程序级别还是平台上。

本章将涵盖以下主题:

  • 网络安全与应用程序安全

  • 使用不同方法实施应用程序授权,如 OAuth、客户端认证等

  • 开发安全启用的网络应用程序的要点

网络安全与应用程序安全

在当今的情况下,网络应用程序安全取决于两个主要方面--网络应用程序本身和其部署的平台。您可以将这两个方面分开,因为任何网络应用程序都无法在没有平台的情况下部署。

网络应用程序堆栈

理解平台和应用程序之间的区别非常重要,因为它对安全性有影响。典型的网络应用程序的架构类似于以下图表所示:

大多数网络应用程序依赖于诸如 Apache/HTTP 服务器、Rails、nginx 等的网络服务器,这些服务器实际上根据应用程序的类型处理传入的请求。这些网络服务器跟踪传入的流量;它们还验证请求并相应地做出响应,考虑到所有用户认证都经过验证。在我们的情况下,Flask 充当我们应用程序的网络服务器。

应用程序 - 平台中的安全替代方案

如前所述,每个网络应用程序在暴露给外部世界之前都需要部署在某种平台上。应用程序平台提供了应用程序所需的协议支持,用于在网络上进行通信。TCP 和在很大程度上 HTTP 都是在应用程序级别处理的。

在软件架构的网络堆栈中,有两个不同的层,包括容易受到网络应用程序攻击的协议,即应用程序平台。这些层如下:

  • 传输

  • 应用程序

让我们详细了解这些层。

传输协议

开放系统互连模型(OSI模型)中,传输层通常被称为第 4 层。网络应用程序使用 TCP 协议作为其传输协议,因为它们具有可靠性。

TCP(传输控制协议)中,每个数据包都受到严密监控,并且具有内置的错误恢复机制,这在通信失败的情况下非常有用。这些机制被利用来攻击网络应用程序。

最常见的攻击是SYN 洪水攻击,这是一种 TCP 请求确认攻击。SYN 洪水攻击通过使用空闲会话与应用程序服务器建立连接,并不断请求直到服务器耗尽资源,无法再处理更多请求。

为了避免此类攻击,系统管理员(开发人员在这里没有控制权)应设置与超时和空闲行为相关的配置,考虑对客户的影响。这类攻击的另一个例子是Smurf 攻击(请参考此链接了解更多详情:en.wikipedia.org/wiki/Smurf_attack)。

安全传输协议

在 OSI 网络模型中,我们还有一些第 5 层的协议,可以使您的网络更安全可靠--SSL/TLS。然而,这一层也存在一些漏洞(例如,SSL 中的 Heartbleed,2014 年,以及 TLS 中的中间人重协议攻击,2009 年)。

应用程序协议

在 OSI 网络模型的第 7 层(最顶层),实际的应用程序驻留并使用 HTTP 协议进行通信,这也是大多数应用程序攻击发生的地方。

HTTP(超文本传输协议)主要有这两个组件:

  • 元数据:HTTP 头包含元数据,对于应用程序和平台都很重要。一些头的例子包括 cookies、content-type、status、connection 等。

  • 行为:这定义了客户端和服务器之间的行为。有一个明确定义的消息如何在 HTTP 客户端(如浏览器)和服务器之间交换的流程。

这里的主要问题是,一个应用程序通常没有内置的能力来识别可疑行为。

例如,客户端通过网络访问 Web 应用程序,可能会受到基于消耗的拒绝服务(DoS)攻击。在这种攻击中,客户端故意以比正常速度慢的速率接收数据,以尝试保持连接时间更长。由于这个原因,Web 服务器的队列开始填充,并消耗更多资源。如果所有资源都用于足够的开放连接,服务器可能会变得无响应。

应用程序-应用程序逻辑中的安全威胁

在本节中,我们将研究不同的方法来验证用户,并确保我们的应用程序只能被真实实体访问。

Web 应用程序安全替代方案

为了保护我们的应用程序免受外部威胁,这里描述了一些替代方法。通常,我们的应用程序没有任何智能来识别可疑活动。因此,以下是一些重要的安全措施描述:

  • 基于 HTTP 的身份验证

  • OAuth/OpenID

  • Windows 身份验证

基于 HTTP 的身份验证

客户端对用户名和密码进行哈希处理,并发送到 Web 服务器,就像我们为我们的 Web 应用程序设置的那样,如下面的屏幕截图所示:

上述的屏幕截图是我们在第六章中创建的 UI,使用 Flux 创建可扩展的 UI。它由后端服务(微服务)和用户数据库进行身份验证,用户数据库存储在 MongoDB 数据库服务器中。此外,在验证用户登录到主页时,用户数据从 MongoDB 集合中读取,然后对用户进行身份验证以进一步进入应用程序。以下是调用的 API 的代码片段:

    @app.route('/login', methods=['POST']) 
    def do_admin_login(): 
     users = mongo.db.users 
     api_list=[] 
     login_user = users.find({'username': request.form['username']}) 
     for i in login_user: 
       api_list.append(i) 
      print (api_list) 
      if api_list != []: 
        #print (api_list[0]['password'].decode('utf-8'),
         bcrypt.hashpw(request.form['password'].encode('utf-8'),
         api_list[0]['password']).decode('utf-8')) 
       if api_list[0]['password'].decode('utf-8') == 
         bcrypt.hashpw(request.form['password'].encode('utf-8'),
         api_list[0]['password']).decode('utf-8'): 
           session['logged_in'] = api_list[0]['username'] 
           return redirect(url_for('index')) 
           return 'Invalide username/password!' 
       else: 
         flash("Invalid Authentication") 

      return 'Invalid User!' 

这是在应用程序级别设置安全性的一种方式,以便应用程序数据可以得到保护。

OAuth/OpenID

OAuth 是授权的开放标准,在允许用户使用第三方凭据进行身份验证的网站中非常常见,通常是电子邮件 ID。

以下是使 OAuth 比其他安全措施更好的一些关键特性:

  • 它与任何操作系统或安装无关

  • 它简单易用

  • 它更可靠并提供高性能

  • 它专门为需要集中身份验证方法的分布式系统设计

  • 这是一个免费使用的基于开源的身份提供者服务器软件

  • 它支持基于云的身份提供者,如 Google、Auth0、LinkedIn 等

  • 它也被称为 SSO(单一登录或基于令牌的身份验证)

设置管理员帐户

OAuth 没有服务来授予JWTJSON Web Token,一种用于在各方之间传输声明的 URL 安全 JSON 格式)。您可以在jwt.io/introduction/了解更多关于 JWT 的信息。

身份提供者负责为依赖第三方授权的 Web 应用程序对用户进行身份验证。

您可以根据自己的喜好选择任何身份提供者,因为它们之间的功能是相似的,但在功能方面会有所不同。在本章中,我将向您展示如何使用 Google Web 应用程序(这是来自 Google 的开发者 API)和 Auth0 第三方应用程序进行身份验证。

使用 Auth0 帐户设置

在这个部分,我们将在 Google 开发者工具中设置一个用于身份验证的帐户,并在一个名为Auth0auth0.com)的第三方免费应用程序中设置。

让我们在 Auth0(auth0.com)中启动帐户设置,唯一的要求是需要一个电子邮件 ID 进行注册或注册。请参考以下截图:

一旦您注册/注册了 Auth0 帐户,您将看到以下屏幕:

前面的屏幕是仪表板,我们可以在其中看到用户登录到应用程序的登录活动。它还展示了用户的登录尝试,并记录了用户的活动。简而言之,仪表板可以让您了解应用程序的用户活动。

现在我们需要为我们的应用程序添加一个新的客户端,所以点击“+NEW CLIENT”按钮进行创建。一旦您点击“+NEW CLIENT”按钮,将会出现以下屏幕:

前面的截图是自解释的--您需要为客户端提供一个用户定义的名称(通常名称应与应用程序相关)。此外,您需要选择应用程序的类别。回到我们的案例,我已经给出了名称My App,并选择了第二个选项,即单页 Web 应用程序,因为我们正在使用其中提到的技术。或者,您也可以选择常规 Web 应用程序--它也可以正常工作。这些类别用于区分我们正在编写的应用程序的种类,因为很可能我们可能在一个帐户下开发数百个应用程序。

单击“CREATE”按钮以继续创建客户端。创建完成后,您将看到以下屏幕:

在前面截图中看到的部分中,有许多自动生成的设置,我们需要将它们与我们的 Web 应用程序集成。以下是其中一些部分的定义:

  • 客户端 ID:这是分配给特定应用程序的唯一 ID

  • :这类似于身份验证服务器,在应用程序登录时将被调用

  • 客户端密钥:这是一个秘密密钥,应该保密,不要与任何人分享,因为这可能会导致安全漏洞

  • 客户端类型:这定义了应用程序的类型

  • 允许的回调 URL:这指定了用户身份验证后允许的回调 URL,例如http://localhost:5000/callback

  • 允许的注销 URL:这定义了在用户注销时允许访问的 URL,例如http://localhost:5000/logout

  • 令牌端点身份验证方法:这定义了身份验证的方法,可以是无、或者 post、或者基本

Auth0 帐户的其他功能可能对管理您的应用程序有用,如下所示:

  • SSO 集成:在这个部分,你可以设置与 Slack、Salesforce、Zoom 等其他第三方应用程序的 SSO 登录!

  • 连接:这定义了你想为你的应用定义的认证类型,比如数据库(用户名-密码数据库)、社交(与社交媒体网站如谷歌、LinkedIn 等现有账户集成)、企业(用于企业应用如 AD、谷歌应用等)、或者无密码(通过短信、电子邮件等)。默认情况下,用户名-密码认证是启用的。

  • APIs:在这个部分,你可以管理你的应用的Auth0 管理 API,并进行测试,如下截图所示:

  • 日志:这个部分跟踪你在 Auth0 账户上的活动,对于调试和在威胁时识别可疑活动非常有用。参考以下截图以了解更多关于日志的信息:

这些是 Auth0 账户的最重要功能,可以帮助你以高效的方式管理你的 Web 应用程序安全。

现在,我们的 Auth0 管理员账户已经设置好,准备与我们的 Web 应用集成。

设置谷歌 API 账户

谷歌 API 使用 OAuth 2.0 协议进行认证和授权。谷歌支持常见的 OAuth 2.0 场景,比如用于 Web 服务器、安装和客户端应用程序的场景。

首先,使用你的谷歌账户登录到谷歌 API 控制台(console.developers.google.com)以获取 OAuth 客户端凭据,比如客户端 ID、客户端密钥等。你将需要这些凭据来与你的应用集成。一旦你登录,你将看到以下屏幕:

前面的屏幕展示了谷歌库 API 为其不同的谷歌产品提供的服务。现在,点击左侧面板中的凭据,导航到下一个屏幕,如下截图所示:

现在,点击创建凭据,然后点击 OAuth 客户端 ID 选项,以启动从 API 管理器生成客户端凭据。

现在我们需要提供一些关于我们应用的信息;你必须记住这些细节,这些是我们在 OAuth 账户创建时提供的。一旦准备好,并填写了必填字段,点击创建以生成凭据。

一旦客户端 ID 创建完成,你将看到以下屏幕,其中包含与客户端 ID 相关的信息(凭据):

记住,绝对不要与任何人分享客户端 ID 的详细信息。如果你这样做了,立即重置。现在我们的谷歌 API 账户已经准备好与我们的 Web 应用集成了。

将 Web 应用与 Auth0 账户集成

为了将 Auth0 账户与我们的应用集成,我们需要为我们的回调创建一个新的路由。这个路由将在用户从 Auth0 账户进行认证后设置会话。因此,让我们将以下代码添加到app.py文件中:

    @app.route('/callback') 
    def callback_handling(): 
      code = request.args.get('code') 
      get_token = GetToken('manishsethis.auth0.com') 
      auth0_users = Users('manishsethis.auth0.com') 
      token = get_token.authorization_code(os.environ['CLIENT_ID'], 
                                        os.environ['CLIENT_SECRET'],
      code, 'http://localhost:5000/callback') 
      user_info = auth0_users.userinfo(token['access_token']) 
      session['profile'] = json.loads(user_info) 
    return redirect('/dashboard') 

正如你在前面的代码中看到的,我使用了我们从 Auth0 账户控制台获取的客户端凭据。这些是我们在客户端创建时生成的凭据。

现在让我们添加路由/仪表板,用户在认证后被重定向到该路由:

    @app.route("/dashboard") 
    def dashboard(): 
      return render_template('index.html', user=session['profile']) 

前面的路由简单地调用index.html,并将会话详细信息作为参数传递给index.html

现在我们需要修改我们的index.html来通过 Auth0 触发身份验证。有两种触发方式。第一种是将 Auth0 域作为登陆页面,这意味着一旦他们访问 URL(http://localhost:5000),用户将被重定向到 Auth0 账户的登陆页面。另一种方式是通过提供一个按钮来手动触发。

在本章的范围内,我们将使用手动触发,其中 Auth0 账户可以作为登录应用程序的替代方式。

让我们在login.html中添加以下代码。此代码将在登录页面上显示一个按钮,如果您点击该按钮,它将触发 Auth0 用户注册页面:

   <center><button onclick="lock.show();">Login using Auth0</button>
     </center> 
   <script src="img/lock.min.js"> 
     </script> 
   <script> 
    var lock = new Auth0Lock(os.environ['CLIENT_ID'],
     'manishsethis.auth0.com', { 
      auth: { 
       redirectUrl: 'http://localhost:5000/callback', 
       responseType: 'code', 
       params: { 
         scope: 'openid email' // Learn about scopes:
         https://auth0.com/docs/scopes 
        } 
       } 
     }); 
   </script> 

在我们测试应用程序之前,我们还需要处理一件事情--如何使我们的应用程序了解会话详细信息。

由于我们的index.html获取会话值并在我们的主页上展示它们,因此它用于管理用户的推文。

因此,请按照以下方式更新index.html的 body 标签:

     <h1></h1> 
     <div align="right"> Welcome {{ user['given_name'] }}</div> 
     <br> 
     <div id="react"></div> 

之前的代码需要在用户界面上显示用户的全名。接下来,您需要按照以下方式更新localStorage会话详细信息:

    <script> 
      // Check browser support 
      if (typeof(Storage) !== "undefined") { 
     // Store 
      localStorage.setItem("sessionid","{{ user['emailid'] }}" ); 
     // Retrieve 
      document.getElementById("react").innerHTML =  
      localStorage.getItem("sessionid"); 
      } else { 
        document.getElementById("react").innerHTML = "Sorry, your 
        browser does not support Web Storage..."; 
      } 
    </script> 

我们现在几乎完成了。我希望您记得,当您在我们的微服务 API 中为特定用户发布推文时,我们已经设置了身份验证检查。我们需要删除这些检查,因为在这种情况下,我们使用 Auth0 进行身份验证。

太棒了!运行您的应用程序,并查看是否可以在http://localhost:5000/看到以下屏幕:

接下来,点击“使用 Auth0 登录”按钮,以获取 Auth0 登录/注册面板,如下图所示。

提供所需的详细信息,然后点击立即注册,它将在 Auth0 帐户中注册。请记住,在这种情况下,您不会看到任何通过电子邮件直接登录的方式,因为我们使用用户名密码进行身份验证。如果您想直接通过电子邮件注册,那么您需要在社交连接部分启用 google-OAuth2 方式扩展。一旦您启用它,您将能够看到您的注册页面如下:

一旦您成功注册,您将被重定向到主页,在那里您可以发布推文。如果您看到以下屏幕,那就意味着它起作用了:

在这里需要注意的一件重要的事情是,对于每个注册,都会在您的 Auth0 帐户中创建一个用户详细信息,如下图所示:

太棒了!现在您的应用程序已与 Auth0 帐户集成,您可以跟踪使用您的应用程序的用户。

将您的 Google API 与 Web 应用程序集成

将您的 Google API 与您的 Web 应用程序集成与我们在 Auth0 集成中看到的非常相似。您需要按照接下来列出的步骤进行 Google API 的集成:

  1. 收集 OAuth 凭据:如在 Google API 客户端设置中讨论的那样,我们已经生成了客户端凭据。我们需要捕获诸如客户端 ID、客户端密钥等详细信息。

  2. 从 Google 授权服务器获取访问令牌:在您的应用程序用户可以登录并访问私人数据之前,它需要生成由 Google 提供的身份验证令牌,该令牌充当用户的验证器。单个访问令牌可以授予对多个 API 的不同程度的访问权限。范围参数包含有关用户将具有访问权限的程度的信息,即用户可以从哪些 API 中查看数据。令牌的请求取决于您的应用程序的开发方式。

  3. 将令牌保存到 API:一旦应用程序接收到令牌,它会将该令牌发送到 Google API HTTP 授权标头。如前所述,该令牌被授权执行基于范围参数定义的一定范围 API 上的操作。

  4. 刷新令牌:定期刷新令牌是最佳实践,以避免任何安全漏洞。

  5. 令牌过期:定期检查令牌过期是一个好习惯,这使得应用程序更加安全;这是强烈推荐的。

由于我们正在开发基于 Python 的应用程序,您可以按照以下链接的文档 URL,了解有关在以下链接实现 Google-API 令牌身份验证的信息:

developers.google.com/api-client-library/python/guide/aaa_oauth

一旦用户经过身份验证并开始使用应用程序,您可以在 API 管理器(console.developers.google.com/apis/)上监视用户登录活动,如下所示:

使用谷歌进行身份验证设置略微困难,并需要监督。这就是为什么开发人员选择使用像 Auth0 这样的工具,它可以直接与谷歌集成。

Windows 身份验证

历史上,即使应用程序部署在内部或私有云上,也更倾向于用于局域网和企业网站。然而,出于许多原因,这并不适合云原生安全选项。

有关 Windows 身份验证的更多信息,请访问链接en.wikipedia.org/wiki/Integrated_Windows_Authentication。我们已展示了这些安全方法供您了解,但我们的身份验证方法保持不变。

开发安全启用的 Web 应用程序

随着万维网WWW)上 Web 应用程序的增加,对应用程序安全性的担忧也在增加。现在,我们心中首先出现的问题是为什么我们需要安全启用的应用程序--这个问题的答案是相当明显的。但它的基本原则是什么?以下是我们应该牢记的原则:

  • 如果黑客熟悉应用程序创建时使用的语言,他可以轻易利用您的应用程序。这就是为什么我们启用诸如 CORS 之类的技术来保护我们的代码。

  • 应该只授予组织中非常有限的人员对应用程序及其数据的访问权限。

  • 身份验证和授权是一种保护您的应用程序免受互联网和私人网络威胁的方式。

所有这些因素,或者我应该说,原则,都驱使我们创建安全启用的应用程序。

摘要

在本章中,我们首先定义了不同应用程序堆栈上的安全性,以及根据您的偏好和应用程序要求如何实施或集成不同的应用程序安全措施。

到目前为止,我们已经讨论了应用程序构建。但是从现在开始,我们将完全专注于使用 DevOps 工具将我们的应用程序从开发阶段移至生产阶段的平台构建。因此,事情将变得更加有趣。敬请关注后续章节。

第九章:持续交付

在之前的章节中,我们努力构建我们的应用程序,并为云环境做好准备。由于我们的应用程序现在稳定了,准备好进行首次发布,我们需要开始考虑平台(即云平台)以及可以帮助我们将应用程序移至生产环境的工具。

本章讨论以下主题:

  • 介绍持续集成和持续交付

  • 了解 Jenkins 的持续集成

持续集成和持续交付的演变

现在,很多人都在谈论CI(持续集成)和CD(持续交付),经过审查不同技术人员的观点,我相信每个人对 CI 和 CD 都有不同的理解,对它们仍然存在一些困惑。让我们深入了解并理解它们。

为了理解持续集成,你需要先了解SDLC(系统开发生命周期)和敏捷软件开发过程的背景,这可以帮助你在构建和发布过程中。

了解 SDLC

SDLC 是规划、开发、测试和部署软件的过程。这个过程包括一系列阶段,每个阶段都需要前一个阶段的结果来继续。以下图表描述了 SDLC:

让我们详细了解每个阶段:

  • 需求分析:这是问题分析的初始阶段,业务分析师进行需求分析,并了解业务需求。需求可以是组织内部的,也可以是来自客户的外部的。需求包括问题的范围,可以是改进系统或构建新系统,成本分析和项目目标。

  • 设计:在这个阶段,准备和批准软件解决方案特性的设计。这包括流程图、文档、布局等。

  • 实施:在这个阶段,根据设计进行实际实施。通常,开发人员根据设计阶段定义的目标开发代码。

  • 测试:在这个阶段,开发的代码由QA(质量保证)团队在不同的场景下进行测试。每个模块都使用单元测试和集成测试进行测试。如果测试失败,开发人员会被告知 bug,然后需要修复。

  • 部署/发布:在这个阶段,经过测试的功能被移至生产环境供客户审查。

  • 演进:这个阶段得到客户对开发、测试和发布的升级的审查。

敏捷软件开发过程

敏捷软件开发过程是传统软件开发的替代方案。它更像是一个帮助频繁和高效地发布生产版本的过程,而且 bug 很少。

敏捷过程基于以下原则:

  • 软件升级和客户反馈的持续交付每个阶段

  • 在开发周期的任何阶段都欢迎额外的改进

  • 稳定版本应该频繁发布(每周)

  • 业务团队和开发人员之间的持续沟通

  • 持续改进朝着技术卓越和良好设计

  • 工作软件是进展的主要衡量标准

  • 持续适应不断变化的情况

敏捷软件开发过程是如何工作的?

在敏捷软件开发过程中,完整系统被划分为不同阶段,所有模块或功能都在迭代中交付,来自不同领域的跨职能团队(如规划、单元测试、设计、需求分析、编码等)同时工作。因此,每个团队成员都参与了这个过程,没有一个人闲着,而在传统的 SDLC 中,当软件处于开发阶段时,其余团队要么闲置,要么被低效利用。所有这些使得敏捷过程比传统模式更有优势。以下图表显示了敏捷开发过程的工作流程信息:

在上图中,您不会找到需求分析或设计阶段,因为这些都在高级规划中累积。

以下是敏捷过程中的事件顺序:

  1. 我们从初始规划开始,这为我们提供了关于软件功能的详细信息,然后在高级规划中定义了目标。

  2. 一旦目标确定,开发人员就开始为所需功能编写代码。一旦软件升级准备就绪,测试团队(QA)就开始执行单元测试和集成测试。

  3. 如果发现任何错误,立即修复,然后将代码交付给客户测试(即在阶段或预生产环境)。在这个阶段,代码尚未发布。

  4. 如果代码通过了所有基于客户的测试,这可能是基于 UI 的测试,那么代码就会推送到生产环境;否则,它会再次迭代相同的周期。

现在我们已经了解了敏捷工作流程,让我们了解其优势,这些优势如下所列:

  • 在敏捷开发中,每个功能都可以频繁快速地开发和演示。这里的想法是在部署之前一周左右开发没有错误的功能。这确保了客户对额外功能的满意。

  • 没有专门的开发、测试或其他团队。有一个团队,由 8-10 名成员组成(根据需求),每个成员都能够做任何事情。

  • 敏捷推动团队合作。

  • 它需要最少的文档。

  • 敏捷最适合并行功能开发。

看到了前面的优势,现在公司已经开始在他们的软件开发中采用敏捷 SDLC。

到目前为止,我们一直在研究作为软件开发一部分采用的方法。现在让我们来看看敏捷过程的一个非常关键的方面,即持续集成,这使得我们的开发工作更加轻松。

持续集成

持续集成是将代码合并到主干代码库的过程。简而言之,持续集成帮助开发人员在开发和生成测试结果时通过创建频繁的构建来测试他们的新代码,并且如果一切正常,然后将代码合并到主干代码。

通过以下图表可以理解这一点,它描述了 SDLC 期间出现的问题:

基本上,持续集成期间会出现以下类型的问题:

  • 集成前构建失败

  • 集成失败

  • 构建失败(集成后)

为了解决这些问题,开发人员需要修改代码以修复它,并且整个集成过程会重复,直到功能成功部署。

Jenkins - 一个持续集成工具

Jenkins 是一个开源工具,用于执行持续集成和构建自动化。它与其他任何持续集成工具(如 Bamboo(CirclCI))具有相同的目的,即在开发阶段尽早测试代码。

在 Jenkins 中,您定义了一组指令,用于在不同的应用环境(开发、预生产阶段等)部署您的应用程序。

在继续设置 Jenkins 作业(基本上是项目)并了解 Jenkins 插件之前,让我们首先根据我们的要求设置 Jenkins 并进行配置。

安装 Jenkins

在任何环境中,无论是 Linux(Debian,Red Hat 等),Windows 还是 macOS,Jenkins 的安装都很简单。

先决条件

确保您的 Ubuntu 系统上已安装 Java 8。如果尚未安装,可以按照以下链接中给出的说明进行操作:

medium.com/appliedcode/how-to-install-java-8-jdk-8u45-on-ubuntu-linuxmint-via-ppa-1115d64ae325.

在基于 Debian(Ubuntu)的系统上安装

按照下面列出的步骤在基于 Debian 的系统上安装 Jenkins:

  1. 我们通过执行以下命令将 Jenkins 密钥添加到 APT 软件包列表来开始 Jenkins 安装:
 $ wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | sudo apt-key add -

  1. 接下来,更新源文件,需要与之通信以验证密钥的服务器,如下所示:
      $ sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'

  1. 更新源列表文件后,通过在终端执行以下命令来更新 APT 存储库:
      $ sudo apt-get update -y

  1. 现在我们准备在 Ubuntu 上安装 Jenkins;使用以下命令来执行:
      $ sudo apt-get install jenkins  -y 

  1. 现在安装完成后,请记住 Jenkins 默认运行在端口8080上。但是,如果您想在不同的端口上运行它,那么您需要更新 Jenkins 配置文件(/etc/default/jenkins)中的以下行:
      HTTP_PORT=8080

  1. 接下来,使用此 URL 检查 Jenkins GUI:

请记住,在这种情况下,我们安装了 Jenkins 版本(2.61);之前和即将到来的步骤对于 Jenkins 版本 2.x.x 也是有效的。

如果您看到以下屏幕,这意味着您的安装成功了:

如前面的图像所示,在安装 Jenkins 的系统内有一个存储默认密码的路径。

这证明 Jenkins 已成功安装。

在 Windows 上安装 Jenkins 在 Windows 上的安装非常简单。通常,在 Windows 机器上,Jenkins 不作为服务运行。但是,如果您想将其作为服务启用(这是可选的),您可以按照以下 URL 完整安装 Windows 的 Jenkins 文档:

wiki.Jenkins-ci.org/display/JENKINS/Installing+Jenkins+as+a+Windows+service#InstallingJenkinsasaWindowsservice-InstallJenkinsasaWindowsservice.

配置 Jenkins

现在是时候配置 Jenkins 了,因此,让我们从指定路径(即/var/lib/Jenkins/secrets/initialAdminPassword)中获取密码,将其粘贴到安装向导中提供的空格中,然后单击“继续”。单击“继续”后,您应该看到类似以下屏幕:

在下一个屏幕上,您将看到可以安装我们需要的集成插件的屏幕。现在我们将选择“安装建议的插件”选项。请注意,我们也可以在初始配置后安装其他插件。所以,不用担心!

一旦单击“安装建议的插件”,您将看到以下屏幕,显示插件安装的进度:

插件安装可能需要一段时间。所有这些插件都是 Jenkins 建议的,因为您可能在项目相关工作中需要它们。

插件安装完成后,它会要求您创建一个管理员用户来访问 Jenkins 控制台。请注意,为了设置 Jenkins,我们使用了临时凭据。

输入用户详细信息后,单击“保存并完成”以完成设置。

您的 Jenkins 设置已成功完成。

自动化 Jenkins

在本节中,我们将介绍 Jenkins 配置的不同部分,并将看看如何成功创建我们的第一个作业并构建我们的应用程序。

理想情况下,成功登录后,我们的 Jenkins 主页应该看起来像这样:

保护 Jenkins

强烈建议设置 Jenkins 安全性,使您的控制台安全,因为我们正在将我们的应用程序暴露给 Jenkins。

从 Jenkins 主页,单击“管理 Jenkins”以导航到 Jenkins 的设置部分,然后单击右侧窗格中的“配置全局安全性”以打开安全面板。

在配置全局安全性部分,我们可以管理用户授权,如下面的屏幕截图所示:

如前面的屏幕截图所示,您可以根据其角色为用户定义访问列表。通常,在大型组织中,根据使用情况为不同的人提供用户访问权限,以便维护 Jenkins 安全性。通常,我们要么使用基于 Unix 的用户/组数据库,要么使用 Jenkins 自己的用户数据库。

插件管理

插件管理非常重要,因为这些插件使我们能够将不同的环境(可能是云平台)或本地资源与 Jenkins 集成,并且使我们能够管理资源上的数据,如应用服务器、数据库服务器等。

从管理 Jenkins 面板中,选择管理插件选项以打开管理插件面板,它应该看起来像这样:

在此面板中,您可以安装、卸载和升级系统中的任何特定插件。从同一面板,您还可以升级 Jenkins。

版本控制系统

Jenkins 主要用于构建特定应用程序代码,或在任何基础设施平台上部署代码(即用于持续部署)。

如今,组织将其应用程序代码存储在任何版本控制系统中,例如 Git,管理员具有集中控制,并可以根据用户角色提供所需的访问权限。此外,由于我们正在谈论持续集成,因此建议将应用程序代码存储在具有版本控制的集中位置,以维护代码的完整性。

为了保持版本代码,请确保您从管理插件面板安装 Git 插件。

要通过 Jenkins 克隆 Git 存储库,您需要为 Jenkins 系统输入电子邮件和用户名。为此,请切换到作业目录,并运行以下 Git 配置命令:

# Need to configure the Git email and user for the Jenkins job 

# switch to the job directory 
cd /var/lib/Jenkins/jobs/myjob/workspace 

# setup name and email 
sudo git config user.name "Jenkins" 
sudo git config user.email "test@gmail.com" 

这需要设置以便从存储库下载代码,或在 Git 中合并分支时,以及其他情况下。

设置 Jenkins 作业

现在我们准备设置我们的第一个 Jenkins 作业。如前所述,每个作业都是为执行特定任务而创建的,可以是个别的,也可以是流水线的。

根据 Andrew Phillips 的说法,理想情况下,流水线将软件交付过程分解为各个阶段。每个阶段旨在从不同角度验证新功能的质量,以验证新功能,并防止错误影响用户。如果遇到任何错误,将以报告的形式返回反馈,并确保达到所需的软件质量。

为了启动作业创建,在 Jenkins 主页上,单击左侧的“新项目”,或单击右侧窗格中的“创建新作业”链接:

单击后,它将打开一个向导,询问您的项目/作业名称以及要创建的作业类型,如下面的屏幕截图所示:

描述已经提供,以及项目类型,以便给我们一个 Jenkins 中可用不同选项的概述。这些类型需要被选择,因为它们基于类型有不同的配置。

请注意,由于我们正在使用最新的 Jenkins 版本,可能一些项目类型在旧版本中可能不存在,因此请确保您安装了最新的 Jenkins。

现在,我们将选择自由风格项目,指定一个唯一的作业名称,然后单击“确定”以继续配置我们的作业。单击“确定”后,您将看到以下页面:

在前面的页面中,您可以定义作业的详细信息,例如项目名称、描述、GitHub 项目等。

接下来,单击“源代码管理”选项卡;您将看到以下屏幕:

在前面的部分中,您将定义您的源代码详细信息。如果您之前在配置部分中还没有设置 Jenkins 用户凭据,那么您也需要设置 Jenkins 用户凭据。如果尚未设置,请单击凭据旁边的“添加”按钮。它将打开一个弹出窗口,看起来像这样:

您在此处定义的用户(即管理员)需要在代码存储库中具有访问权限。

有多种方式可以为存储库上的所述用户设置身份验证,这些方式在“种类”(下拉菜单)中定义:

重要的是要注意,Jenkins 将立即测试与所提到的存储库 URL 的凭据。如果失败,它将显示您在此截图中看到的错误:

假设凭据与存储库 URL 匹配,让我们继续单击“构建触发器”选项卡以滚动它。以下屏幕显示了可以对作业进行连续部署的构建触发器选项:

这个构建触发器部分非常重要,因为它决定了您的构建应该运行多频繁,以及触发构建的参数。例如,如果您希望在每次 Git 提交后构建您的应用程序,您可以选择“当更改被推送到 GitBucket 时构建”的选项。

因此,一旦开发人员在存储库的某个分支(通常是主分支)中提交任何更改,那么此作业将自动触发。这就像是在存储库顶部的一个钩子,它会跟踪其中的活动。或者,如果您想要定期构建您的应用程序或运行此作业,那么您可以指定类似于这样的条件-- H/15 * * * *--在轮询 SCM 以安排中,这意味着此作业将每 15 分钟运行一次。这类似于我们通常在基于 Linux 的系统中设置的 cron 作业。

接下来的两个部分,构建环境构建,是为与工作区相关的任务定义的。由于我们正在处理一个基于 Python 的应用程序,并且我们已经构建了我们的应用程序,所以我们现在可以跳过这些部分。但是,如果您有一个用 Java 编写的应用程序或.NET 应用程序,您可以使用 ANT 和 Maven 构建工具,并分支构建。或者,如果您想构建一个基于 Python 的应用程序,那么可以使用诸如 pyBuilder (pybuilder.github.io/)之类的工具。以下屏幕显示了构建选项:

完成后,您可以单击下一个选项卡,即后构建操作。这用于定义构建成功后需要执行的操作。由于这一部分,Jenkins 也可以用作持续部署工具。因此,在此后构建操作中,您可以指定应用程序需要部署的平台,例如 AWS EC2 机器、Code deploy、Azure VM 或其他平台。

在持续集成的后构建部分中,我们还可以执行诸如成功构建后的 Git 合并、在 Git 上发布结果等操作。此外,您还可以为利益相关者设置电子邮件通知,以便通过电子邮件向他们提供有关构建结果的更新。有关更多详细信息,请参见以下截图:

就这样。一旦填写了必要的细节,点击保存以保存配置。现在您已经准备好构建您的应用程序了--点击左侧面板中的立即构建链接,如下面的截图所示:

注意:对于第一次构建执行,如果您尚未设置轮询 SCM 或构建触发器部分,则需要手动触发它。

这是我们目前从 Jenkins 那里得到的有关作业创建的所有信息。然而,在接下来的章节中,我们将使用 Jenkins 作为持续交付和持续集成工具,部署我们在之前章节中创建的 React 应用程序到 AWS、Azure 或 Docker 等不同平台上。我们还将看到 AWS 服务与 Jenkins 的集成,通过单次提交自动化将应用程序交付到 GitHub 存储库。

理解持续交付

持续交付是一种软件工程实践,其中生产就绪的功能被生产并部署到生产环境。

持续交付的主要目标是在不考虑平台的情况下执行成功的应用程序部署,无论是大规模分布式系统还是复杂的生产环境。

在跨国公司中,我们始终确保应用程序代码处于稳定且可部署状态,即使有许多开发人员同时在不同的应用程序组件上工作。在持续交付中,我们还确保单元测试和集成测试成功进行,使其达到生产就绪状态。

持续交付的需求

人们普遍认为,如果我们尝试更频繁地部署软件,我们应该预期系统的稳定性和可靠性水平会降低,但这并不完全正确。持续交付提供了一些实践,为愿意在竞争激烈的市场中发布稳定可靠软件的组织提供了令人难以置信的竞争优势。

持续交付的实践给我们带来了以下重要的好处:

  • 无风险发布:软件发布中的主要要求是最小或零停机时间。毕竟,这始终与业务有关,用户不应因频繁发布而受到影响。通过使用 BlueGreenDeployment(martinfowler.com/bliki/BlueGreenDeployment.html)等模式,我们可以在部署过程中实现零停机时间。

  • 竞争市场:在持续交付中,所有团队,如构建和部署团队、测试团队、开发人员等,都共同合作,使不同的活动如测试、集成等每天都发生。这使得功能发布过程更快(一周或两周),我们将频繁地将功能发布到生产环境供客户使用。

  • 质量改进:在持续交付中,开发人员无需担心测试过程,因为流水线会处理这一过程,并向 QA 团队展示结果。这使得 QA 团队和开发人员能够更仔细地进行探索性测试、可用性测试以及性能和安全性测试,从而改善客户体验。

  • 更好的产品:通过在构建、测试、部署和环境设置中使用持续交付,我们减少了软件增量变更的成本和交付成本,从而使产品在一段时间内变得更好。

持续交付与持续部署

持续交付和持续部署在构建、测试和软件发布周期方面相似,但在流程方面略有不同,您可以从以下图表中了解到:

在下一章中,我们将讨论基于容器技术的 Docker。我相信你们大多数人之前都听说过 Docker,所以请继续关注对 Docker 的深入了解。我们下一章见!

在持续部署中,经过所有测试检查的生产就绪代码直接部署到生产环境,这使得软件发布频繁。但在持续交付的情况下,除非由相关部门手动触发或批准,否则不会部署生产就绪的应用程序代码。

总结

在整个章节中,我们讨论了像 Jenkins 这样的 CI 和 CD 工具,并且也看了它们的不同功能。在这个阶段理解这些工具非常重要,因为大多数处理云平台的公司都使用这些流程进行软件开发和部署。所以,现在您已经了解了部署流水线,您可以开始了解我们将部署应用程序的平台了。

(图片已省略)

第十章:将您的服务 Docker 化

既然我们已经从上一章了解了持续集成和持续交付/部署,现在是深入研究基于容器的技术,比如 Docker,我们将在其中部署我们的应用程序的正确时机。在本章中,我们将看一下 Docker 及其特性,并在 Docker 上部署我们的云原生应用。

本章将涵盖以下主题:

  • 了解 Docker 及其与虚拟化的区别

  • 在不同操作系统上安装 Docker 和 Docker Swarm

  • 在 Docker 上部署云原生应用

  • 使用 Docker Compose

了解 Docker

Docker 是一个容器管理系统CMS),它使您能够将应用程序与基础架构分离,这样更容易开发、部署和运行应用程序。它对管理Linux 容器LXC)很有用。这让您可以创建镜像,并对容器执行操作,以及对容器运行命令或操作。

简而言之,Docker 提供了一个在被称为容器的隔离环境中打包和运行应用程序的平台,然后在不同的软件发布环境中进行部署,如阶段、预生产、生产等。

Docker与任何传统 VM相比都更轻量,如下图所示:

关于 Docker 与虚拟化的一些事实

仍然成功地在传统 VM 上工作的组织有很多。话虽如此,有些组织已经将他们的应用程序迁移到了 Docker 上,或者准备这样做。以下是 Docker 比虚拟机具有更多潜力的几个原因:

  • 在比较 Docker 和虚拟机时,Docker 的系统开销比虚拟机低。

  • 其次,在 Docker 环境中的应用程序通常比虚拟机性能更高。

  • 而 VM 软件技术名为Hypervisor,它充当 VM 环境和底层硬件之间的代理,提供必要的抽象层;在 Docker 中,我们有 Docker 引擎,它比 Docker 机器给我们更多的控制。

  • 此外,正如您在上图中所看到的,Docker 在 Docker 环境中共享主机操作系统,而虚拟机需要自己的操作系统进行应用程序部署。这使得 Docker 更轻量化,可以更快地启动和销毁,与虚拟机相比。Docker 类似于在主机操作系统上运行的任何其他进程。

  • 在云原生应用的情况下,我们需要在每个开发阶段之后快速测试我们的微服务,Docker 将是一个很好的平台选项来测试我们的应用程序,这是强烈推荐的。

Docker Engine - Docker 的支柱

Docker Engine 是一个客户端服务器应用程序,具有以下组件:

  • Dockerd:这是一个守护进程,以在主机操作系统的后台持续运行,以跟踪 Docker 容器属性,如状态(启动/运行/停止)

  • Rest API:这提供了与守护程序交互并在容器上执行操作的接口

  • Docker 命令行:这提供了命令行界面来创建和管理 Docker 对象,如镜像、容器、网络和卷

设置 Docker 环境

在本节中,我们将看一下在不同操作系统上安装 Docker 的过程,比如 Debian 和 Windows 等。

在 Ubuntu 上安装 Docker

设置 Docker 非常简单。市场上主要有两个版本的 Docker。

Docker Inc.拥有容器化Docker 产品,将 Docker 商业支持CS)版更名为 Docker 企业版EE),并将 Docker Engine 转换为 Docker 社区版CE)。

EE 和 CE 有一些变化;显然,商业支持是其中之一。但是,在 Docker 企业版中,他们围绕容器内容、平台插件等构建了一些认证。

在本书中,我们将使用 Docker 社区版,因此我们将从更新 APT 存储库开始:

$ apt-get update -y 

现在,让我们按照以下步骤从 Docker 官方系统添加 GPG 密钥:

$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D 

然后让我们将 Docker 存储库添加到 Ubuntu 的 APT 源列表中:

$ sudo apt-add-repository 'deb https://apt.dockerproject.org/repo ubuntu-xenial main' 

有时,在 Ubuntu 14.04/16.04 中找不到apt-add-repository实用程序。为了安装所述的实用程序,请使用以下命令安装software-properties-common软件包:$ sudo apt-get install software-properties-common -y

接下来,更新 APT 软件包管理器以下载最新的 Docker 列表,如下所示:

$ apt-get update -y

如果您想从 Docker 存储库而不是默认的 14.04 存储库下载并安装 Docker Engine,请使用以下命令:

$ apt-cache policy docker-engine

您将在终端上看到以下输出:

现在,我们准备安装我们的 Docker Engine,所以让我们执行以下命令来安装它:

$ sudo apt-get install -y docker-engine -y 

由于 Docker 依赖于一些系统库,可能会遇到类似于以下截图显示的错误:

如果遇到此类错误,请确保已安装这些库并且版本已定义。

Docker Engine 安装成功后,现在是时候通过执行以下命令来验证它了:

$ docker -v   
Docker version 17.05.0-ce, build 89658be 

如果您看到类似于前面终端显示的版本,则我们可以开始了。

要获取有关 Docker 的帮助,可以执行以下命令:

$ docker help 

如果您真的想使用 Docker 企业版,可以按照官方 Docker 网站上显示的安装步骤进行操作(docs.docker.com/engine/installation/linux/ubuntu/)。

在 Windows 上安装

理想情况下,Windows 不适合 Docker,这就是为什么您在 Windows 系统上看不到容器技术的原因。话虽如此,我们有一些解决方法。其中之一是使用 Chocolatey。

为了使用 Chocolatey 在 Windows 系统上安装 Docker,请按照以下步骤进行操作:

  1. 从官方网站安装 Chocolatey(chocolatey.org/install)。

在前面的链接中显示了安装 Chocolatey 的几种方法。

  1. 安装了 Chocolatey 后,您只需在 cmd 或 PowerShell 中执行以下命令:
 $ choco install docker

这将在 Windows 7 和 8 操作系统上安装 Docker。

同样,如果您想使用 Docker 企业版,可以按照此链接中显示的步骤进行操作:

docs.docker.com/docker-ee-for-windows/install/#install-docker-ee

设置 Docker Swarm

Docker Swarm 是 Docker 机器池的常用术语。 Docker Swarm 非常有用,因为它可以快速扩展或缩小基础架构,用于托管您的网站。

在 Docker Swarm 中,我们可以将几台 Docker 机器组合在一起,作为一个单元共享其资源,例如 CPU、内存等,其中一台机器成为我们称之为领导者的主机,其余节点作为工作节点。

设置 Docker 环境

在本节中,我们将通过从 Docker 机器中选择领导者并将其余机器连接到领导者来设置 Docker Swarm。

假设

以下是 Docker 环境的一些假设:

  • 我们将使用两台机器,可以是虚拟机或来自云平台的实例,以演示为目的命名为 master 和 node1。此外,我们已经按照 Docker 安装部分中描述的过程在这两台机器上安装了 Docker。

  • 端口2377必须打开以便主节点和节点 1 之间进行通信。

  • 确保应用程序访问所需的端口应该是打开的;我们将需要端口80来使用 nginx,就像我们的示例中一样。

  • 主 Docker 机器可以基于任何类型的操作系统,例如 Ubuntu、Windows 等。

现在,让我们开始我们的 Docker Swarm 设置。

初始化 Docker 管理器

此时,我们需要决定哪个节点应该成为领导者。让我们选择主节点作为我们的 Docker 管理器。因此,请登录到主机并执行以下命令,以初始化此机器为 Docker Swarm 的领导者:

$ docker swarm init --advertise-addr master_ip_address 

此命令将设置提供的主机为主(领导者),并为节点生成一个连接的令牌。请参考以下输出:

需要记住的一些重要点:

  • 不要与任何人分享您的令牌和 IP 地址

  • 其次,在故障转移的情况下可能会有多个主节点

将节点 1 添加到主节点

现在我们已经选择了领导者,我们需要添加一个新节点到集群中以完成设置。登录到节点 1 并执行前面命令输出中指定的以下命令:

$ docker swarm join     --token SWMTKN-1-
1le69e43paf0vxyvjdslxaluk1a1mvi5lb6ftvxdoldul6k3dl-
1dr9qdmbmni5hnn9y3oh1nfxp    master-ip-address:2377 

您可以参考以下截图输出:

这意味着我们的设置是成功的。让我们检查它是否已添加到主 Docker 机器中。

执行以下命令进行验证:

$ docker node ls 

测试 Docker Swarm

既然我们已经设置了 Docker Swarm,现在是时候在其上运行一些服务了,比如 nginx 服务。在主 Docker 机器上执行以下命令,以在端口80上启动您的 nginx 服务:

$ docker service create  --detach=false -p 80:80 --name webserver
nginx 

前面命令的输出应该类似于以下截图:

让我们使用以下 Docker 命令来查看我们的服务是否正在运行:

$ docker service ps webserver 

前面命令的输出应该类似于以下截图:

其他一些验证命令如下:

要验证哪些服务正在运行以及在哪个端口上,请使用以下命令:

$ docker service ls 

如果您看到类似以下截图的输出,那么一切正常:

要扩展服务的 Docker 实例,请使用以下命令:

$ docker service scale webserver=3 

通过访问其默认页面来检查我们的 nginx 是否已经启动。尝试在浏览器中输入http://master-ip-address:80/。如果您看到以下输出,则您的服务已成功部署:

太棒了!在接下来的部分,我们将在 Docker 机器上部署我们的云原生应用程序。

在 Docker 上部署应用程序

在本节中,我们将部署我们的云原生应用程序,这是我们在前几章中开发的。然而,在我们开始创建应用程序架构之前,有一些 Docker 的概念是应该了解的,其中一些如下:

  • Docker 镜像:这些基本上是库和部署在其上的应用程序的组合。这些图像可以从 Docker Hub 公共存储库下载,或者您也可以创建自定义图像。

  • Dockerfile:这是一个配置文件,用于构建可以在以后运行 Docker 机器的图像。

  • Docker Hub:这是一个集中的存储库,您可以在其中保存图像,并可以在团队之间共享。

我们将在应用部署过程中使用所有这些概念。此外,我们将继续使用我们的 Docker Swarm 设置来部署我们的应用程序,因为我们不想耗尽资源。

我们将遵循这个架构来部署我们的应用程序,我们将我们的应用程序和 MongoDB(基本上是应用程序数据)部署在单独的 Docker 实例中,因为建议始终将应用程序和数据分开:

构建和运行我们的 MongoDB Docker 服务

在本节中,我们将创建 Dockerfile 来构建MongoDB,其中将包含所有信息,例如基本图像、要公开的端口、如何安装MongoDB服务等。

现在,让我们登录到您的 Docker 主(领导)帐户,并使用以下内容创建名为Dockerfile的 Docker 文件:

    # MongoDB Dockerfile 
    # Pull base image. 
    FROM ubuntu 
    MAINTAINER Manish Sethi<manish@sethis.in> 
    # Install MongoDB. 
    RUN \ 
    apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 
    7F0CEB10 && \ 
    echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart
    dist 10gen' > /etc/apt/sources.list.d/mongodb.list && \ 
    apt-get update && \ 
    apt-get install -y mongodb-org && \ 
    rm -rf /var/lib/apt/lists/* 

    # Define mountable directories. 
    VOLUME ["/data/db"] 

    # Define working directory. 
    WORKDIR /data 

    # Define default command. 
    CMD ["mongod"] 

    # Expose ports. 
    EXPOSE 27017 
    EXPOSE 28017 

保存它,在我们继续之前,让我们了解其不同的部分,如下所示:

    # Pull base image. 
    FROM ubuntu 

上面的代码将告诉您从 Docker Hub 拉取 Ubuntu 公共图像,并将其作为基础图像运行以下命令:

    # Install MongoDB 
    RUN \ 
    apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 
    7F0CEB10 && \ 
    echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart 
    dist 10gen' > /etc/apt/sources.list.d/mongodb.list && \ 
    apt-get update && \ 
    apt-get install -y mongodb-org && \ 
    rm -rf /var/lib/apt/lists/*

上面的代码部分类似于我们手动执行这些命令为MongoDB;但是,在这种情况下,Docker 会自动处理。

接下来是卷部分,这在某种程度上是可选的。它正在创建可挂载的目录,我们可以在其中存储数据以在外部卷中保持安全。

    # Define mountable directories. 
    VOLUME ["/data/db"] 

接下来的部分是通过这些端口公开的,用户/客户端将能够与 MongoDB 服务器进行通信:

    EXPOSE 27017 
    EXPOSE 28017 

保存文件后,执行以下命令构建图像:

$ docker build --tag mongodb:ms-packtpub-mongodb

构建图像可能需要大约 4-5 分钟,这取决于互联网带宽和系统性能。

以下屏幕显示了 Docker 构建命令的输出:

在前面的截图中,由于显示了成功构建,现在您可以查看图像列表以验证是否存在具有所述标记名称(ms-packtpub-mongodb)的图像。

使用以下命令列出图像:

$ docker images

以下屏幕列出了可用的 Docker 图像:

太棒了!我们的图像已经存在。现在让我们使用以下命令在主 Docker 机器上运行mongodb服务:

$ docker run -d -p 27017:27017 -p 28017:28017 --name mongodb mongodb:ms-packtpub-mongodb mongod --rest --httpinterface

在输出中,您将获得一个随机的 Docker ID,如下面的截图所示:

通过执行docker ps命令来检查 Docker 容器的状态。它的输出应该类似于以下截图:

很少有开发人员和系统管理员知道mongoDB服务有一个 HTTP 接口,我们使用端口28017进行了暴露。

因此,如果我们尝试在浏览器中访问http://your-master-ip-address:28017/,我们将看到类似于以下截图的屏幕:

太棒了!我们的 MongoDB 现在已经运行起来了!

在我们继续为应用程序启动容器之前,让我们了解一下 Docker Hub 对我们有何用处。

Docker Hub - 它是关于什么的?

根据 Docker Hub 官方文档,Docker Hub 是一个基于云的注册表服务,允许您链接到代码存储库,构建图像并对其进行测试,并存储手动推送的图像,并链接到 Docker Cloud,以便您可以将图像部署到您的主机。

简而言之,Docker Hub 是一个集中存储图像的地方,全球任何人都可以访问,只要他们具有所需的权限,并且可以执行围绕图像的操作,以在其主机上部署和运行其应用程序。

Docker Hub 的优点如下:

  • Docker Hub 提供了自动创建构建的功能,如果源代码存储库中报告了任何更改

  • 它提供了 WebHook,用于在成功推送到存储库后触发应用程序部署

  • 它提供了创建私有工作空间以存储图像的功能,并且只能在您的组织或团队内部访问

  • Docker Hub 与您的版本控制系统(如 GitHub、BitBucket 等)集成,这对持续集成和交付非常有用

现在,让我们看看如何将我们的自定义MongoDB图像推送到我们最近创建的私有存储库。

首先,您需要在hub.docker.com创建一个帐户并激活它。一旦登录,您需要根据自己的喜好创建私有/公共存储库,如下所示:

单击“创建”按钮设置仓库,您将被重定向到以下屏幕:

Docker Hub 在免费帐户上只提供一个私有仓库。

现在我们已经创建了仓库,让我们回到我们的主 Docker 机器并执行以下命令:

$ docker login

这将要求您输入 Docker Hub 帐户的凭据,如下截图所示:

登录成功后,是时候使用以下命令为要推送到仓库的镜像打标签了:

$ docker tag mongodb:ms-packtpub-mongodb manishsethis/docker-packtpub

如果我们不指定标签,那么它将默认使用最新的标签。

标签创建完成后,是时候将标签推送到仓库了。使用以下命令来执行:

$ docker push manishsethis/docker-packtpub

以下屏幕显示了 Docker push命令的输出:

推送完成后,您将在 Docker Hub 的“标签”选项卡中看到镜像,如此处所示:

这意味着您的镜像已成功推送。

要拉取此镜像,您只需使用以下命令:

$ docker pull manishsethis/docker-packtpub

哦,哇!这太简单了,只要您有凭据,就可以从任何地方访问它。

还有其他 Docker 注册表提供者,如 AWS(EC2 容器注册表)、Azure(Azure 容器注册表)等。

目前,这就是我们从 Docker Hub 那边得到的全部内容。在本章中,我们将继续使用 Docker Hub 来推送镜像。

现在继续,我们准备将我们的云原生应用部署到另一个容器中,但在此之前,我们需要使用 Dockerfile 为其构建一个镜像。因此,让我们创建一个名为app的目录,并创建一个空的 Dockerfile,其中包含以下内容:

 FROM ubuntu:14.04 
    MAINTAINER Manish Sethi<manish@sethis.in> 

    # no tty 
    ENV DEBIAN_FRONTEND noninteractive 

    # get up to date 
    RUN apt-get -qq update --fix-missing 

    # Bootstrap the image so that it includes all of our dependencies 
    RUN apt-get -qq install python3  python-dev python-virtualenv
 python3-pip --assume-yes 
    RUN sudo apt-get install build-essential autoconf libtool libssl-
 dev libffi-dev --assume-yes 
 # Setup locale 
    RUN export LC_ALL=en_US.UTF-8 
    RUN export LANG=en_US.UTF-8 
    RUN export LANGUAGE=en_US.UTF-8 

    # copy the contents of the cloud-native-app(i.e. complete
 application) folder into the container at build time 
 COPY cloud-native-app/ /app/ 

    # Create Virtual environment 
    RUN mkdir -p /venv/ 
    RUN virtualenv /venv/ --python=python3 

    # Python dependencies inside the virtualenv 
    RUN /venv/bin/pip3 install -r /app/requirements.txt 

    # expose a port for the flask development server 
    EXPOSE 5000 

    # Running our flask application  
    CMD cd /app/ && /venv/bin/python app.py 

我相信我之前已经解释了 Dockerfile 中大部分部分,尽管还有一些部分需要解释。

  COPY cloud-native-app/ /app/ 

在 Dockerfile 的前面部分,我们将应用程序的内容,即代码,从本地机器复制到 Docker 容器中。或者,我们也可以使用 ADD 来执行相同的操作。

CMD是我们想要在 Docker 容器内执行的命令的缩写,它在 Dockerfile 中定义如下:

# Running our flask application  
CMD cd /app/ && /venv/bin/python app.py 

现在保存文件并运行以下命令来构建镜像:

$ docker build --tag cloud-native-app:latest .

这可能需要一些时间,因为需要安装和编译许多库。每次更改后构建镜像是一个好习惯,以确保镜像与当前配置更新。输出将类似于此处显示的输出:

确保构建过程的每个部分都成功。

现在我们已经准备好了我们的镜像,是时候使用最新的镜像启动我们的容器了。

执行以下命令来启动容器,并始终记住要暴露端口5000以访问我们的应用程序:

$ docker run -d -p 5000:5000  --name=myapp  cloud-native-app:latest

现在运行docker ps命令来检查容器状态:

正如你所看到的,myapp容器中有两个容器在运行:我们的应用程序在运行,而mongodb容器中将运行您的mongodb服务。

接下来,检查应用程序的 URL(http://your-master-ip-address:5000/)。如果看到以下屏幕,这意味着我们的应用程序已成功部署,并且我们在 Docker 上已经上线了:

现在我们可以通过创建新用户并登录,然后发布推文来测试我们的应用程序。我不会再次执行,因为我们在创建应用程序时已经执行过了。

根据经验,我知道应用程序和数据库(即 MongoDB)之间的通信可能会存在一些挑战,因为应用程序和数据库都在单独的容器中,可能在单独的网络中。为了解决这种问题,您可以创建一个网络,并将两个容器连接到该网络。

举例来说,如果我们需要为我们的容器(myappmongodb)这样做,我们将按照以下步骤进行:

  1. 使用以下命令创建一个单独的网络:
      $ docker network create -d bridge --subnet 172.25.0.0/16
      mynetwork

  1. 现在我们已经创建了网络,可以使用以下命令将两个容器添加到这个网络中:
      $ docker network connect mynetwork  myapp
      $ docker network connect mynetwork  mongodb

  1. 为了找到分配给这些容器的 IP,我们可以使用以下命令:
      $ docker inspect --format '{{ .NetworkSettings.IPAddress }}'
      $(docker ps -q)

这个网络的创建是一种设置应用程序和数据库之间通信的替代方式。

好了,我们已经在 Docker 上部署了我们的应用程序,并了解了它的不同概念。唯一剩下的概念是 Docker Compose。让我们了解一下它是什么,以及它与其他工具有何不同。

Docker Compose

根据官方 Docker Compose 网站(docs.docker.com/compose/overview/),Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。使用 Compose,您可以使用 Compose 文件来配置应用程序的服务。

简单来说,它帮助我们以更简单和更快的方式构建和运行我们的应用程序。

在前一节中,我们部署应用程序并构建镜像时,首先创建了一个 Dockerfile,然后执行了Docker build命令来构建它。一旦构建完成,我们通常使用docker run命令来启动容器,但是,在 Docker Compose 中,我们将定义一个包含配置细节的.yml文件,例如端口、执行命令等。

首先,Docker Compose 是一个独立于 Docker Engine 的实用程序,可以根据您所使用的操作系统类型使用以下链接进行安装:

https://docs.docker.com/compose/install/

安装完成后,让我们看看如何使用 Docker Compose 来运行我们的容器。假设我们需要使用 Docker Compose 运行云原生应用程序容器。我们已经为其生成了 Dockerfile,并且应用程序也在相同的位置(路径)上。

接下来,使用以下内容,我们需要在与 Dockerfile 相同的位置创建一个Docker-compose.yml文件:

    #Compose.yml 
    version: '2' 
    services: 
    web: 
     build: . 
      ports: 
      - "5000:5000" 
      volumes: 
       - /app/ 
     flask: 
      image: "cloud-native-app:latest" 

docker-compose.yml中添加配置后,保存并执行docker-compose up命令。构建镜像后,我们将看到以下输出:

此外,如果查看容器的状态,您会发现多个容器(在我们的情况下,app_web-1app_flask_1)由 compose 启动,这就是为什么它对于需要大规模基础设施的多容器应用程序非常有用,因为它创建了类似 Docker Swarm 的 Docker 机器集群。以下屏幕显示了 Docker 机器的状态:

太棒了!我们还通过 Docker-compose 部署了我们的应用程序。现在您可以尝试访问应用程序的公共 URL(your-ip-address:5000)来确认成功部署应用程序。

最后,确保将您的镜像推送到 Docker Hub 以将其保存在集中式存储库中。由于我们已经推送了 MongoDB 镜像,请使用以下命令来推送cloud-native-app镜像:

$ docker tag cloud-native-app:latest manishsethis/docker-packtpub:cloud-native-app
$ docker push manishsethis/docker-packtpub:cloud-native-app

我们应该看到类似的输出,用于 Docker push命令如下:

总结

在本章中,我们首先看了一个最有趣的技术--Docker--,它是基于容器的。我们研究了 Docker 周围的不同概念,已经部署了我们的应用程序,并研究了我们如何通过 Docker 来管理它。我们还探索了使用 Docker Compose 和 Dockerfile 部署应用程序的多种方式。

在接下来的章节中,情况将变得更加有趣,因为我们最终将接触到云平台,根据我们的应用程序在平台上构建基础设施,并尝试部署它。所以,请继续关注下一章!到时见。

第十一章:在 AWS 平台上部署

在上一章中,我们看到了我们应用程序的一个平台,名为 Docker。它可以隔离您的应用程序,并可用于响应来自客户的应用程序请求。在本章中,我们将向您介绍云平台,特别是 AWS(亚马逊云服务),主要涉及 IaaS(基础设施)和 PaaS(平台即服务)服务。我们还将看看如何构建基础设施并部署我们的应用程序。

本章包括以下主题:

  • 介绍 AWS 及其服务

  • 使用 Terraform/CloudFormation 构建应用程序基础设施

  • 使用 Jenkins 进行持续部署

开始使用亚马逊云服务(AWS)

亚马逊云服务(AWS)是一个安全的云平台。它在 IaaS 和 PaaS 方面提供各种服务,包括计算能力、数据库存储和内容传递,有助于扩展应用程序,并在全球范围内发展我们的业务。AWS 是一个公共云,根据云计算概念,它以按需交付和按使用量付费的方式提供所有资源。

您可以在aws.amazon.com/了解更多关于 AWS 及其服务的信息。

如在第一章中指定的,介绍云原生架构和微服务,您需要创建一个 AWS 账户才能开始使用这些服务。您可以使用以下链接创建一个账户:

medium.com/appliedcode/setup-aws-account-1727ce89353e

登录后,您将看到以下屏幕,其中展示了 AWS 及其类别。一些服务处于测试阶段。我们将使用与计算和网络相关的一些服务来构建我们应用程序的基础设施:

一些常用的 AWS 应用服务如下:

  • EC2(弹性计算云):这是 AWS 提供的计算服务,简单来说,它提供了一个服务器。

  • ECS(弹性容器服务):这类似于位于公共云(即亚马逊)上的 Docker 服务。它仅在 EC2 机器上管理 Docker。您可以在亚马逊云中轻松设置 Docker 集群,而不是在本地创建 Docker 集群,而且开销更小。

  • EBS(弹性 Beanstalk):这是一个 PaaS 服务,您只需上传代码,并指定需要多少基础设施(基本上是应用服务器(EC2))。EBS 将负责创建机器,并在其上部署代码。

  • S3(简单存储服务):这是 AWS 提供的存储服务,我们通常将应用程序数据或静态内容存储在其中,可用于静态网站托管。我们将用它进行持续部署。

  • Glacier:这是另一个存储服务,主要用于备份,因为成本较低,因此数据存储和检索能力较慢,与 S3 相比。

  • VPC(虚拟专用网络):这是一个网络服务,可以让您控制资源的可访问性。我们将使用此服务来保护我们的基础设施。此服务非常有用,可用于保护我们的应用程序服务和数据库服务,并仅向外部世界公开所需的选择性资源。

  • CloudFront:这是一个内容传递服务,可以在全球范围内分发您在 S3 中的内容,并确保无论请求源的位置如何,都可以快速检索到。

  • CloudFormation:这为开发人员和系统管理员提供了一种简单的方式来创建和管理一组相关的 AWS 资源,例如以代码形式进行配置和更新。我们将使用此服务来构建我们的基础设施。

  • CloudWatch:此服务跟踪您的资源活动。它还以日志的形式跟踪 AWS 账户上的任何活动,这对于识别任何可疑活动或账户被盗非常有用。

  • IAM(身份和访问管理):这项服务正如其名,非常有用于管理 AWS 账户上的用户,并根据他们的使用和需求提供角色/权限。

  • Route 53:这是一个高可用和可扩展的云 DNS 云服务。我们可以将我们的域名从其他注册商(如 GoDaddy 等)迁移到 Route 53,或者购买 AWS 的域名。

AWS 提供了许多其他服务,本章无法涵盖。如果您有兴趣并希望探索其他服务,可以查看 AWS 产品列表(aws.amazon.com/products/)。

我们将使用大部分前述的 AWS 服务。让我们开始按照我们的应用程序在 AWS 上构建基础设施。

在 AWS 上构建应用程序基础设施

在我们的应用程序的这个阶段,系统架构师或 DevOps 人员进入画面,并提出不同的基础设施计划,这些计划既安全又高效,足以处理应用程序请求,并且成本效益高。

就我们的应用程序而言,我们将按照以下图像构建其基础设施:

我们将按照前述的应用程序架构图为我们的应用程序构建基础设施,其中包括一些 AWS 服务,如 EC2、VPC、Route 53 等。

有三种不同的方式可以在 AWS 云上配置您的资源,分别是:

  • 管理控制台:这是我们已经登录的用户界面,可以用于在云上启动资源。(查看此链接以供参考:console.aws.amazon.com/console/

  • 编程方式:我们可以使用一些编程语言,如 Python、Ruby 等来创建资源,为此 AWS 创建了不同的开发工具,如 Codecom。此外,您可以使用 SDK 根据您喜欢的语言创建资源。您可以查看aws.amazon.com/tools/ 了解更多信息。

  • AWS CLI(命令行界面):它是建立在 Python SDK 之上的开源工具,提供与 AWS 资源交互的命令。您可以查看链接:docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html 了解其工作原理,并了解在您的系统上设置此工具的步骤。

创建资源非常简单直接,所以我们不会涵盖这一点,但您可以查看 AWS 文档(aws.amazon.com/documentation/)来了解如何操作。

我将向您展示如何使用 Terraform 和名为 CloudFormation 的基于 AWS 的服务构建基础设施。

生成身份验证密钥

身份验证是任何产品或平台的重要功能,用于检查试图访问产品并执行操作的用户的真实性,同时保持系统安全。由于我们将使用 API 访问 AWS 账户,我们需要授权密钥来验证我们的请求。现在,一个重要的 AWS 服务进入了叫做IAM(身份和访问管理)的画面。

在 IAM 中,我们定义用户并生成访问/密钥,并根据我们想要使用它访问的资源分配角色。

强烈建议永远不要以根用户身份生成访问/密钥,因为默认情况下它将对您的账户拥有完全访问权限。

以下是创建用户和生成访问/密钥的步骤:

  1. 转到console.aws.amazon.com/iam/home?region=us-east-1#/home;您应该看到以下屏幕:

  1. 现在,点击左窗格中的第三个选项,名为用户。如果您的帐户是新的,您将看不到用户。现在,让我们创建一个新用户--为此,请点击右窗格中的“添加用户”按钮:

  1. 一旦您点击“添加用户”按钮,将加载一个新页面,并要求输入用户名以及您希望用户访问帐户的方式。例如,如果您打算仅将此用户manish用于编程目的,那么建议您取消选中 AWS 管理控制台访问框,以便用户无需使用 AWS 管理控制台登录。参考以下截图:

  1. 完成后,点击屏幕右下方的“下一步:权限”按钮。接下来,您需要选择要授予此用户的权限,我们称之为 IAM 策略。这意味着现在用户应该能够根据定义的策略访问资源,以及用户在资源上允许的操作类型。现在,我们向此用户添加“Power User Access”策略。

  2. 在内部,Power User Access 将具有 JSON 格式的策略,类似于这样:

     { 
       "Version": "2012-10-17", 
       "Statement": [ 
            { 
              "Effect": "Allow", 
              "NotAction": [ 
                "iam:*", 
                "organizations:*" 
              ], 
              "Resource": "*" 
            }, 
            { 
              "Effect": "Allow", 
                "Action": "organizations:DescribeOrganization", 
                "Resource": "*" 
            } 
          ] 
      } 

有关 IAM 策略的更多信息,请阅读以下链接中的文档:docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html

使用 Microsoft Active Directory 的读者可以使用 AD 连接器轻松地将 AD 与 IAM 集成。有关更多信息,请阅读以下链接中提供的文章:aws.amazon.com/blogs/security/how-to-connect-your-on-premises-active-directory-to-aws-using-ad-connector/

考虑以下截图:

  1. 一旦您为用户添加了策略,请点击屏幕右下方的“下一步:审查”按钮以继续。

  2. 下一个屏幕将要求您进行审查,一旦确定,您可以点击“创建用户”按钮来创建用户:

  1. 一旦您点击“创建用户”按钮,用户将被创建,并且策略将附加到用户上。您现在将看到以下屏幕,其中自动生成了访问密钥和秘密密钥,您需要保密,并且绝对不与任何人分享:

  1. 现在我们的访问/秘密密钥已生成,是时候在 AWS 上构建我们的应用程序基础架构了。我们将使用以下工具来实现:
  • Terraform:这是一个用于在不同云平台上构建基础架构的开源工具

  • CloudFormation:这些是使用 AWS 资源构建应用程序基础架构的 AWS 服务

Terraform - 一个构建基础架构的工具

Terraform 是一个用于在不同云平台(如 AWS、Azure 等)上构建、管理和版本化基础架构的工具。它可以管理基础架构的低级组件,如计算、存储、网络等。

在 Terraform 中,我们指定描述应用程序基础架构的资源规范的配置文件。Terraform 描述执行计划和要实现的期望状态。然后,它根据规范开始构建资源,并在构建后跟踪基础架构的当前状态,始终执行增量执行,如果配置发生更改。

以下是 Terraform 的一些特点:

  • Terraform 将您的数据中心描述为蓝图,可以进行版本控制,并可以管理为代码。

  • Terraform 在实际实施之前为您提供执行计划,这有助于您将执行计划与期望结果进行匹配。

  • Terraform 可以帮助您设计所有资源并并行创建资源。它可以让您了解资源之间的依赖关系,并确保在创建资源之前满足这些依赖关系。

  • 凭借其洞察力,它可以让开发人员更好地控制基础架构的修改,减少人为错误。

在 Terraform 中,我们将 AWS 中的每项服务都视为需要创建的资源,因此我们需要为其创建提供其创建所需的强制属性。现在,让我们开始创建资源:

  1. 首先,我们需要创建VPC(虚拟私有云),在其中启动所有其他资源。

注意:根据约定,我们需要按照.tf文件扩展名创建所有文件。

  1. 所以,让我们创建一个空的main.tf文件。添加以下代码,用于设置服务提供商的访问和秘钥以进行身份验证:
    # Specify the provider and access details 
        provider "aws" { 
          region = "${var.aws_region}" 
          access_key = "${var.aws_access_key}" 
          secret_key = "${var.aws_secret_key}" 
     } 

  1. 正如您在前面的代码中所看到的,有一个值${var.aws_region}。实际上,这是一个约定,将所有值保存在一个名为variables.tf的单独文件中,所以我们在这里这样做。让我们用以下内容更改variables.tf文件:
     variable "aws_access_key" { 
          description = "AWS access key" 
          default = ""                    # Access key 
      } 

     variable "aws_secret_key" { 
         description = "AWS secret access key" 
         default = ""                          # Secret key 
      } 

      variable "aws_region" { 
          description = "AWS region to launch servers." 
          default = "us-east-1" 
      } 

  1. 接下来,我们需要创建 VPC 资源,所以让我们将以下代码添加到main.tf中:
      # Create a VPC to launch our instances into 
        resource "aws_vpc" "default" { 
          cidr_block = "${var.vpc_cidr}" 
          enable_dns_hostnames = true 
          tags { 
            Name = "ms-cloud-native-app" 
          } 
      } 

  1. 我们使用了一个变量,需要在variables.tf中定义如下:
       variable "vpc_cidr"{ 
          default = "10.127.0.0/16"             # user defined 
       } 

  1. 一旦定义了 VPC 资源,我们需要创建一个子网,该子网将与 EC2 机器、弹性负载均衡器或其他资源关联。因此,将以下代码添加到main.tf中:
        # Create a subnet to launch our instances into 
        resource "aws_subnet" "default" { 
          vpc_id                  = "${aws_vpc.default.id}" 
          cidr_block              = "${var.subnet_cidr}" 
          map_public_ip_on_launch = true 
        } 

      Now, define the variable we have used in above code in 
      variables.tf 
      variable "subnet_cidr"{ 
       default = "10.127.0.0/24" 
      } 

  1. 由于我们希望我们的资源可以从互联网访问,因此我们需要创建一个互联网网关,并将其与我们的子网关联,以便其中创建的资源可以通过互联网访问。

注意:我们可以创建多个子网来保护我们资源的网络。

  1. 将以下代码添加到main.tf中:
     # Create an internet gateway to give our subnet access to the
     outside world 
     resource "aws_internet_gateway" "default" { 
     vpc_id = "${aws_vpc.default.id}" 
      } 

     # Grant the VPC internet access on its main route table 
     resource "aws_route" "internet_access" { 
     route_table_id         = "${aws_vpc.default.main_route_table_id}" 
     destination_cidr_block = "0.0.0.0/0" 
     gateway_id             = "${aws_internet_gateway.default.id}" 

  1. 接下来,我们需要确保您将启动 EC2 机器的子网为机器提供公共地址。这可以通过将下面给出的代码添加到您的main.tf中来实现:
     # Create a subnet to launch our instances into 
     resource "aws_subnet" "default" { 
       vpc_id                  = "${aws_vpc.default.id}" 
       cidr_block              = "${var.subnet_cidr}" 
       map_public_ip_on_launch = true 
     } 

  1. 一旦配置完成,就该开始创建应用服务器和 MongoDB 服务器了。

  2. 最初,我们需要创建依赖资源,例如安全组,否则无法启动 EC2。

  3. 将以下代码添加到main.tf中以创建安全组资源:

    # the instances over SSH and HTTP 
    resource "aws_security_group" "default" { 
    name        = "cna-sg-ec2" 
    description = "Security group of app servers" 
    vpc_id      = "${aws_vpc.default.id}" 

    # SSH access from anywhere 
    ingress { 
     from_port   = 22 
     to_port     = 22 
     protocol    = "tcp" 
     cidr_blocks = ["0.0.0.0/0"] 
    } 

    # HTTP access from the VPC 
    ingress { 
      from_port   = 5000 
      to_port     = 5000 
      protocol    = "tcp" 
      cidr_blocks = ["${var.vpc_cidr}"] 
     } 

     # outbound internet access 
     egress { 
      from_port   = 0 
      to_port     = 0 
      protocol    = "-1" 
      cidr_blocks = ["0.0.0.0/0"] 
    } 
   } 

  1. 在这个安全组中,我们只打开225000端口,以便进行 ssh 和访问我们的应用程序。

  2. 接下来,我们需要添加/创建 ssh 密钥对,您可以在本地机器上生成并上传到 AWS,也可以从 AWS 控制台生成。在我们的情况下,我使用ssh-keygen命令在本地机器上生成了一个 ssh 密钥。现在,为了在 AWS 中创建 ssh 密钥对资源,将以下代码添加到main.tf中:

   resource "aws_key_pair" "auth" { 
     key_name   = "${var.key_name}" 
      public_key = "${file(var.public_key_path)}" 
   }   

  1. 添加以下代码片段到variables.tf文件中以为变量提供参数:
    variable "public_key_path" { 
      default = "ms-cna.pub" 
    } 

  1. 现在我们已经创建了依赖资源,是时候创建应用服务器(即 EC2 机器)了。因此,将以下代码片段添加到main.tf中:
    resource "aws_instance" "web" { 
     # The connection block tells our provisioner how to 
     # communicate with the resource (instance) 
      connection { 
       # The default username for our AMI 
        user = "ubuntu" 
        key_file = "${var.key_file_path}" 
        timeout = "5m" 
      } 
     # Tags for machine 
     tags {Name = "cna-web"} 
     instance_type = "t2.micro" 
     # Number of EC2 to spin up 
      count = "1" 
      ami = "${lookup(var.aws_amis, var.aws_region)}" 
      iam_instance_profile = "CodeDeploy-Instance-Role" 
      # The name of our SSH keypair we created above. 
      key_name = "${aws_key_pair.auth.id}" 

     # Our Security group to allow HTTP and SSH access 
     vpc_security_group_ids = ["${aws_security_group.default.id}"] 
     subnet_id = "${aws_subnet.default.id}" 
    } 

  1. 我们在 EC2 配置中使用了一些变量,因此需要在variables.tf文件中添加变量值:
    variable "key_name" { 
      description = "Desired name of AWS key pair" 
      default = "ms-cna" 
    } 

   variable "key_file_path" { 
      description = "Private Key Location" 
      default = "~/.ssh/ms-cna" 
   } 

    # Ubuntu Precise 12.04 LTS (x64) 
     variable "aws_amis" { 
       default = { 
        eu-west-1 = "ami-b1cf19c6" 
        us-east-1 = "ami-0a92db1d" 
        #us-east-1 = "ami-e881c6ff" 
        us-west-1 = "ami-3f75767a" 
        us-west-2 = "ami-21f78e11" 
     } 
   } 

太好了!现在我们的应用服务器资源配置已经准备好了。现在,我们已经添加了应用服务器配置,接下来,我们需要为 MongoDB 服务器添加类似的设置,这对于保存我们的数据是必要的。一旦两者都准备好了,我们将创建 ELB(这将是用户应用访问的入口点),然后将应用服务器附加到 ELB。

让我们继续添加 MongoDB 服务器的配置。

配置 MongoDB 服务器

为 MongoDB 服务器的创建添加以下代码到main.tf

    resource "aws_security_group" "mongodb" { 
     name        = "cna-sg-mongodb" 
     description = "Security group of mongodb server" 
     vpc_id      = "${aws_vpc.default.id}" 

    # SSH access from anywhere 
    ingress { 
      from_port   = 22 
      to_port     = 22 
      protocol    = "tcp" 
      cidr_blocks = ["0.0.0.0/0"] 
     } 

    # HTTP access from the VPC 
    ingress { 
      from_port   = 27017 
      to_port     = 27017 
      protocol    = "tcp" 
      cidr_blocks = ["${var.vpc_cidr}"] 
     } 
    # HTTP access from the VPC 
     ingress { 
      from_port   = 28017 
      to_port     = 28017 
      protocol    = "tcp" 
      cidr_blocks = ["${var.vpc_cidr}"] 
      } 

    # outbound internet access 
    egress { 
      from_port   = 0 
      to_port     = 0 
      protocol    = "-1" 
      cidr_blocks = ["0.0.0.0/0"] 
     } 
   } 

接下来,我们需要为 MongoDB 服务器添加配置。还要注意,在以下配置中,我们在创建 EC2 机器时提供了 MongoDB 安装的服务器:

    resource "aws_instance" "mongodb" { 
    # The connection block tells our provisioner how to 
    # communicate with the resource (instance) 
    connection { 
     # The default username for our AMI 
     user = "ubuntu" 
     private_key = "${file(var.key_file_path)}" 
     timeout = "5m" 
     # The connection will use the local SSH agent for authentication. 
     } 
    # Tags for machine 
    tags {Name = "cna-web-mongodb"} 
    instance_type = "t2.micro" 
    # Number of EC2 to spin up 
    count = "1" 
    # Lookup the correct AMI based on the region 
    # we specified 
    ami = "${lookup(var.aws_amis, var.aws_region)}" 
    iam_instance_profile = "CodeDeploy-Instance-Role" 
    # The name of our SSH keypair we created above. 
     key_name = "${aws_key_pair.auth.id}" 

     # Our Security group to allow HTTP and SSH access 
     vpc_security_group_ids = ["${aws_security_group.mongodb.id}"] 

     subnet_id = "${aws_subnet.default.id}" 
     provisioner "remote-exec" { 
      inline = [ 
        "sudo echo -ne '\n' | apt-key adv --keyserver 
         hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10", 
       "echo 'deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb- 
        org/3.2 multiverse' | sudo tee /etc/apt/sources.list.d/mongodb-
         org-3.2.list", 
       "sudo apt-get update -y && sudo apt-get install mongodb-org --
       force-yes -y", 
       ] 
     } 
   } 

仍然需要配置的最后一个资源是弹性负载均衡器,它将平衡客户请求以提供高可用性。

配置弹性负载均衡器

首先,我们需要通过将以下代码添加到main.tf来为我们的 ELB 创建安全组资源:

    # A security group for the ELB so it is accessible via the web 
     resource "aws_security_group" "elb" { 
     name        = "cna_sg_elb" 
     description = "Security_group_elb" 
     vpc_id      = "${aws_vpc.default.id}" 

    # HTTP access from anywhere 
    ingress { 
      from_port   = 5000 
      to_port     = 5000 
      protocol    = "tcp" 
      cidr_blocks = ["0.0.0.0/0"] 
     } 

    # outbound internet access 
    egress { 
      from_port   = 0 
      to_port     = 0 
      protocol    = "-1" 
      cidr_blocks = ["0.0.0.0/0"] 
     } 

现在,我们需要添加以下配置来创建 ELB 资源,并将应用服务器添加到其中:

    resource "aws_elb" "web" { 
    name = "cna-elb" 

     subnets         = ["${aws_subnet.default.id}"] 
     security_groups = ["${aws_security_group.elb.id}"] 
     instances       = ["${aws_instance.web.*.id}"] 
     listener { 
       instance_port = 5000 
       instance_protocol = "http" 
       lb_port = 80 
       lb_protocol = "http" 
      } 
     } 

现在,我们已经准备好运行 Terraform 配置了。

我们的基础设施配置已准备就绪,可以部署了。使用以下命令来了解执行计划是一个很好的做法:

$ terraform plan

最后一个命令的输出应该类似于以下截图:

如果您没有看到任何错误,您可以执行以下命令来实际创建资源:

$ terraform apply

输出应该看起来像这样:

目前,我们还没有与我们注册的域,但如果我们已经在 Route 53 中注册并配置了域名,我们需要在main.tf中创建一个额外的资源,为我们的应用添加一个条目。我们可以使用以下代码来实现:

    resource "aws_route53_record" "www" { 
      zone_id = "${var.zone_id}" 
      name = "www.domain.com" 
      type = "A" 
      alias { 
       name = "${aws_elb.web.dns_name}" 
       zone_id = "${aws_elb.web.zone_id}" 
       evaluate_target_health = true 
      } 
    } 

这就是我们需要做的一切。另外,使您的基础设施高度可用的另一种快速且最关键的方法是创建一个基于服务器指标使用(CPU 或内存)的自动扩展服务。我们提供条件来决定是否需要扩展我们的基础设施,以便我们的应用性能应该看到更少的延迟。

为了做到这一点,您可以在www.terraform.io/docs/providers/aws/r/autoscaling_group.html查看 Terraform 文档。

目前,我们的应用尚未部署,我们将使用 Code Deploy 服务使用持续交付来部署我们的应用,我们将在本章的后面部分讨论。

在此之前,让我们看看如何使用 AWS 提供的名为CloudFormation的云平台服务创建相同的设置。

CloudFormation - 使用代码构建基础设施的 AWS 工具

CloudFormation 是 AWS 的一个服务,它的工作方式类似于 Terraform。但是,在 CloudFormation 中,我们不需要访问/秘钥。相反,我们需要创建一个 IAM 角色,该角色将具有启动所需的所有资源的访问权限,以构建我们的应用。

您可以使用 YAML 或 JSON 格式编写您的 CloudFormation 配置。

让我们通过使用 CloudFormation 开始我们的基础设施设置,构建 VPC,在那里我们将创建一个 VPC,一个公共子网和一个私有子网。

让我们创建一个新文件vpc.template,其中 VPC 和子网(公共和私有)的配置如下:

"Resources" : { 

   "VPC" : { 
     "Type" : "AWS::EC2::VPC", 
     "Properties" : { 
       "CidrBlock" : "172.31.0.0/16", 
       "Tags" : [ 
         {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} }, 
         {"Key" : "Network", "Value" : "Public" } 
       ] 
     } 
   }, 
"PublicSubnet" : { 
     "Type" : "AWS::EC2::Subnet", 
     "Properties" : { 
       "VpcId" : { "Ref" : "VPC" }, 
       "CidrBlock" : "172.31.16.0/20", 
       "AvailabilityZone" : { "Fn::Select": [ "0", {"Fn::GetAZs": {"Ref": "AWS::Region"}} ]}, 
       "Tags" : [ 
         {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} }, 
         {"Key" : "Network", "Value" : "Public" } 
       ] 
     } 
   }, 
   "PrivateSubnet" : { 
     "Type" : "AWS::EC2::Subnet", 
     "Properties" : { 
       "VpcId" : { "Ref" : "VPC" }, 
       "CidrBlock" : "172.31.0.0/20", 
       "AvailabilityZone" : { "Fn::Select": [ "0", {"Fn::GetAZs": {"Ref": "AWS::Region"}} ]}, 
       "Tags" : [ 
         {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} }, 
         {"Key" : "Network", "Value" : "Public" } 
       ] 
     } 
   }, 

上述配置是以 JSON 格式编写的,以便让您了解 JSON 配置。此外,我们还需要指定路由表和互联网网关的配置如下:

"PublicRouteTable" : { 
     "Type" : "AWS::EC2::RouteTable", 
     "Properties" : { 
       "VpcId" : {"Ref" : "VPC"}, 
       "Tags" : [ 
         {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} }, 
         {"Key" : "Network", "Value" : "Public" } 
       ] 
     } 
   }, 

   "PublicRoute" : { 
     "Type" : "AWS::EC2::Route", 
     "Properties" : { 
       "RouteTableId" : { "Ref" : "PublicRouteTable" }, 
       "DestinationCidrBlock" : "0.0.0.0/0", 
       "GatewayId" : { "Ref" : "InternetGateway" } 
     } 
   }, 

   "PublicSubnetRouteTableAssociation" : { 
     "Type" : "AWS::EC2::SubnetRouteTableAssociation", 
     "Properties" : { 
       "SubnetId" : { "Ref" : "PublicSubnet" }, 
       "RouteTableId" : { "Ref" : "PublicRouteTable" } 
     } 
   } 
 }, 

现在我们已经有了可用的配置,是时候从 AWS 控制台为 VPC 创建一个堆栈了。

AWS 上的 VPC 堆栈

执行以下步骤,从 AWS 控制台为 VPC 创建一个堆栈:

  1. 转到console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new使用 CloudFormation 创建一个新的堆栈。您应该看到一个如此截图所示的屏幕:

提供模板文件的路径,然后点击“下一步”按钮。

  1. 在下一个窗口中,我们需要指定堆栈名称,这是我们堆栈的唯一标识符,如下所示:

提供堆栈名称,然后点击“下一步”。

  1. 下一个屏幕是可选的;如果我们想设置SNS通知服务)或为其添加 IAM 角色,我们需要在这里添加它:

如果要启用通知和 IAM 角色,请添加详细信息,然后点击“下一步”。

  1. 下一个屏幕是用于审查细节,并确保它们正确以创建堆栈:

准备好后,点击“创建”来启动堆栈创建。在创建时,您可以检查事件以了解资源创建的状态。

您应该会看到一个类似于这样的屏幕:

在前面的屏幕上,您将能够看到堆栈的进度,如果出现错误,您可以使用这些事件来识别它们。

一旦我们的 VPC 堆栈准备好,我们需要在我们的 VPC 中创建 EC2、ELB 和自动缩放资源。我们将使用 YAML 格式来为您提供如何以 YAML 格式编写配置的概述。

您可以在<repository 路径>找到完整的代码。我们将使用main.yml文件,其中包含有关您需要启动实例的 VPC 和子网的详细信息。

  1. 为了启动堆栈,请转到以下链接:

console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new

在启动配置中将会有一个变化--不再在文件中指定值,而是在提供细节的时候在 AWS 控制台中指定,如下所示:

  1. 请参考以下截图,提供您想要部署应用程序的实例细节:

  1. 一旦您在上一个屏幕中提供了所有的细节,向下滚动到下一部分,在那里它将要求 ELB 的细节,如下一张截图所示:

剩下的步骤对于创建 AWS CloudFormation 堆栈来说是一样的。为了添加 MongoDB 服务器,我们需要在main.yml中添加 EC2 机器的配置。

在 AWS CloudFormation 中创建配置是很简单的,因为 AWS 提供了一些模板,我们可以用作创建我们的模板的参考。以下是模板的链接:

aws.amazon.com/cloudformation/aws-cloudformation-templates/

这就是我们为构建基础设施所做的一切;现在是我们的应用程序在应用服务器上部署的时候了。

云原生应用程序的持续部署

在前面的部分,我们成功地设置了基础设施,但我们还没有部署应用程序。此外,我们需要确保进一步的部署应该使用持续部署来处理。由于我们的开发环境在本地机器上,我们不需要设置持续集成周期。然而,对于许多开发人员协作工作的大型公司,我们需要使用 Jenkins 设置一个单独的持续集成管道。在我们的情况下,我们只需要持续部署。我们的持续部署管道将是这样的:

它是如何工作的

它从开发人员将新代码推送到其版本控制系统的主分支开始(在我们的情况下,它是 GitHub)。一旦新代码被推送,Jenkins 的 GitHub 插件根据其定义的工作检测到更改,并触发 Jenkins 作业将新代码部署到其基础设施。然后 Jenkins 与 Code Deploy 通信,触发代码到 Amazon EC2 机器。由于我们需要确保我们的部署是成功的,我们可以设置一个通知部分,它将通知我们部署的状态,以便在需要时可以回滚。

持续部署管道的实施

让我们首先从配置 AWS 服务开始,从 Code Deploy 开始,这将帮助我们在可用的应用服务器上部署应用程序。

  1. 最初,当您切换到代码部署服务时(us-west-1.console.aws.amazon.com/codedeploy/),您应该会看到以下屏幕:

前面的屏幕截图是 Code Deploy 的介绍页面,展示了其功能。

  1. 点击页面中间的“立即开始”按钮以继续前进。

  2. 接下来,您应该看到以下屏幕,该屏幕将建议您部署一个示例应用程序,这对于初始阶段来说是可以的。但是由于我们已经建立了基础设施,在这种情况下,我们需要选择自定义部署--这将跳过演练。因此,选择该选项,然后单击“下一步”。

  1. 点击“跳过演练”以继续前进。

  2. 在下一个向导屏幕中,有几个需要审查的部分。

第一部分将要求您创建应用程序--您需要提供用户定义的应用程序名称和部署组名称,这是强制性的,因为它成为您的应用程序的标识符:

  1. 向下滚动到下一部分,该部分讨论您希望为应用程序选择的部署类型。有两种方法,定义如下:
  • 蓝/绿部署:在这种类型中,在部署过程中,会启动新实例并向其部署新代码,如果其健康检查正常,则会替换为旧实例,然后旧实例将被终止。这适用于生产环境,客户无法承受停机时间。

  • 原地部署:在这种部署类型中,新代码直接部署到现有实例中。在此部署中,每个实例都会脱机进行更新。

我们将选择原地部署,但选择会随着用例和产品所有者的决定而改变。例如,像 Uber 或 Facebook 这样的应用程序,在部署时无法承受停机时间,将选择蓝/绿部署,这将为它们提供高可用性。

  1. 让我们继续下一节,讨论应用程序将要部署的基础设施。我们将指定实例和 ELB 的详细信息,如此屏幕截图所示:

  1. 在下一部分中,我们将定义部署应用程序的方式。例如,假设您有 10 个实例。您可能希望一次在所有这些实例上部署应用程序,或者一次一个,或者一次一半。我们将使用默认选项,即CodeDeployDefault.OneAtATime

在本节中,我们还需要指定一个服务角色,Code Deploy 需要该角色来在您的 AWS 资源上执行操作,更具体地说是在 EC2 和 ELB 上。

要了解更多有关服务角色创建的信息,请转到此链接的 AWS 文档:docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create.html

  1. 提供所需信息后,点击“创建应用程序”。

一旦您的应用程序准备就绪,您将看到以下屏幕:

现在我们已经准备好部署。我们只需要在 Jenkins 中创建一个作业,并添加一个带有 CodeDeploy 详细信息的后置构建部分。

作业的创建类似于我们在上一章中解释的内容。但是需要进行以下几个更改:

  1. 首先,我们需要确保已安装了一些 Jenkins 插件,即 AWS CodeDeploy Plugin for Jenkins,Git 插件,GitHub 插件等。

  2. 安装了插件后,您应该在后置构建操作列表中看到新的操作,如下面的屏幕截图所示:

  1. 接下来,您需要选择“部署应用程序到 AWS CodeDeploy”操作。将添加一个新部分,我们需要提供在 AWS 控制台中创建的 CodeDeploy 应用程序的详细信息,如此屏幕截图所示:

  1. 我们还需要提供在本章开头部分创建的访问/秘钥,即生成认证密钥。这是必要的,因为 Jenkins 在打包应用程序后需要将其上传到 S3,并指示 CodeDeploy 从指定的存储桶部署最新构建。

我们需要做的就是这些。现在我们的 Jenkins 作业已经准备好部署应用程序了。试一下,应该会像黄油一样顺利。

总结

这一章在各个方面都非常有趣。首先,你对 AWS 服务有了基本的了解,以及如何充分利用它们。接下来,我们探讨了我们在 AWS 云上应用程序的架构,这将塑造你对未来可能计划创建的不同应用程序/产品的架构设计的看法。我们还使用了 Terraform,这是一个第三方工具,用于将基础架构构建为 AWS 代码。最后,我们部署了我们的应用程序,并使用 Jenkins 创建了一个持续的部署流水线。在下一章中,我们将探索微软拥有的另一个云平台--Microsoft Azure。保持活力,准备好在接下来的章节中探索 Azure。到时见!

第十二章:在 Azure 平台上实施

在上一章中,我们看到了一个用于托管我们的应用程序的云计算平台--AWS--其中包含了所有功能,以使应用程序具有高可用性,并且没有停机时间。在本章中,我们将讨论另一个名为Microsoft Azure的云平台。

本章包括以下主题:

  • 介绍 Microsoft Azure

  • 构建应用程序基础设施 Azure 服务

  • 使用 Jenkins 与 Azure 进行 CI/CD

开始使用 Microsoft Azure

正如其名称所示,Microsoft Azure 是由微软拥有的公共云平台,为其客户提供不同的 PaaS 和 IaaS 服务。一些流行的服务包括虚拟机、应用服务、SQL 数据库、资源管理器等。

Azure 服务主要分为这两个类别:

  • 平台服务:这些是为客户提供环境来构建、管理和执行他们的应用程序,同时自行处理基础架构的服务。以下是一些 Azure 服务按其各种类别:

  • 管理服务:这些提供了管理门户和市场服务,提供了 Azure 中的图库和自动化工具。

  • 计算:这些是诸如 fabric、函数等服务,帮助开发人员开发和部署高度可扩展的应用程序。

  • CDN 和媒体:这些分别提供全球范围内安全可靠的内容传递和实时流媒体。

  • Web +移动:这些是与应用程序相关的服务,如 Web 应用程序和 API 应用程序,主要用于 Web 和移动应用程序。

  • 分析:这些是与大数据相关的服务,可以帮助机器学习开发人员进行实时数据处理,并为您提供有关数据的见解,如 HDInsight、机器学习、流分析、Bot 服务等。

  • 开发工具:这些服务用于版本控制、协作等。它包括 SDK 等。

  • AI 和认知服务:这些是基于人工智能的服务,例如语音、视觉等。一些提供此类服务的服务包括文本分析 API、认知等。

  • 基础设施服务:这些是服务提供商负责硬件故障的服务。服务器的定制是客户的责任。客户还管理其规格:

  • 服务器计算和容器:这些是虚拟机和容器等服务,为客户应用程序提供计算能力。

  • 存储:这些分为两种类型--BLOB 和文件存储。根据延迟和速度提供不同的存储能力。

  • 网络:这些提供了一些与网络相关的服务,如负载均衡器和虚拟网络,可以帮助您保护网络,并使其对客户响应更加高效。

以下图表将更好地理解 Azure 平台:

您可以在以下链接详细查看所有 Microsoft Azure 产品提供:

azure.microsoft.com/en-in/services/

要开始使用 Microsoft Azure,您需要拥有一个账户。由于本章涉及在 Azure 上实施我们的应用程序,我们不会介绍如何创建账户。如果您需要帮助,可以阅读以下链接中的文章,这将确实帮助您:

medium.com/appliedcode/setup-microsoft-azure-account-cbd635ebf14b

Azure 提供了一些基于 SaaS 的服务,您可以在azuremarketplace.microsoft.com/en-us上查看。

关于 Microsoft Azure 基础知识的几点

一旦您准备好并登录到您的 Azure 账户,您将被重定向到 Azure 门户(portal.azure.com),它将展示 Azure 服务。最初,Azure 提供了一个免费账户,并为您的使用提供了价值为 200 美元的信用额,有效期为 30 天。微软 Azure 也支持按需付费模式,当您用完所有免费信用后,可以切换到付费账户。

以下是您在继续之前应该了解的一些 Azure 基本概念:

  • Azure 资源管理器: 最初,Azure 基于一种称为ASM(Azure 服务管理器)的部署模型。在最新版本的 Azure 中,采用了ARM(Azure 资源管理器),它提供了高可用性和更灵活性。

  • Azure 区域: 全球分布约 34 个区域。

  • Azure 区域列表可在azure.microsoft.com/en-us/regions/上找到。

  • 特定区域所有服务的列表可在azure.microsoft.com/en-us/regions/services/上找到。

  • Azure 自动化: Azure 提供了许多模板在不同的基于 Windows 的工具中,如 Azure-PowerShell,Azure-CLI 等。您可以在github.com/Azure找到这些模板。

由于 Azure 是由微软拥有的,我们将主要在 Azure 控制台(UI)上工作,并通过它创建资源。Azure 环境非常适合喜欢在 Windows 系统上部署他们的应用程序的开发人员或 DevOps 专业人员,他们的应用程序是用.NET 或 VB 编写的。它还支持最新的编程语言,如 Python,ROR 等。

对于喜欢在 Microsoft 产品上工作的人来说,Microsoft Azure 是理想的选择,比如 Visual Studio。

使用 Azure 架构我们的应用基础设施

一旦您进入 Azure 门户,您应该在屏幕上看到以下默认仪表板:

现在是时候在 MS Azure 上架构我们的应用基础设施了。我们将按照下面给出的架构图创建我们在 Azure 上的生产环境:

在这个架构中,我们将使用一些 Azure 服务,它们如下:

  • 虚拟机: 这类似于我们在 AWS 中的 EC2 机器。我们将在虚拟机中部署我们的应用程序和 MongoDB 服务器。

  • 虚拟网络: 虚拟网络在 AWS 中等同于 VPC,需要创建以保持我们的通信网络安全。

  • 存储: 每个虚拟机都由一个存储账户支持,我们不需要显式创建,因为它会随着虚拟机一起创建来存储您的数据。

  • 负载均衡器: 这个负载均衡器的使用与 AWS 中的负载均衡器相同,但它们在算法上有轻微的变化,因为 Azure 主要遵循基于哈希的平衡或源 IP 算法,而 AWS 遵循轮询算法或粘性会话算法。

  • DNS: 当我们有域名注册时,DNS 很有用,我们需要从 Azure 管理我们的 DNS。在云平台中,我们称之为区域

  • 子网: 我们将在虚拟网络中创建一个子网,以区分我们的资源,这些资源需要面向互联网或不需要。

  • 自动扩展: 我们在图中没有提到这一点,因为它取决于您的应用需求和客户响应。

因此,让我们开始创建我们的应用服务器(即虚拟机),我们的应用程序将驻留在其中。

正如我之前提到的,Azure 有一个非常用户友好的 UI,它会根据您定义的资源在后台创建程序代码,并使用资源管理器将其提供给您,这使得 DevOps 工程师的工作更加轻松。

在 Azure 中创建虚拟机

按照下面列出的步骤在 Microsoft Azure 中创建一个虚拟机:

  1. 转到 Azure 仪表板,并在左侧面板中选择 新建 以启动 VM 向导,如下截图所示:

  1. 现在我们需要选择要启动的操作系统。我们将在列表中选择 Ubuntu Server 16.04 LTS 服务器选项(我们选择此选项,因为我们的应用程序是在 Ubuntu 操作系统上开发的)。

在接下来的屏幕中,我们需要选择部署模型。有两种部署模型可用。它们是经典型(标准 VM)和资源管理器(高可用性 VM)。选择资源管理器模型,如下截图所示,然后点击 创建 按钮继续:

  1. 在下一个屏幕上,我们需要提供 VM 的用户名和身份验证方法,如下截图所示;点击 确定 继续:

  1. 接下来,我们需要根据需求选择虚拟机的大小。我们将选择标准型的 DS1_V2。选择它,然后点击页面底部的 选择 按钮,如下所示:

  1. 在下一个屏幕中,我们将定义一些可选细节,如网络、子网、公共 IP 地址、安全组、监视等:

每次创建虚拟网络时,都建议创建一个虚拟网络,并通过单击虚拟网络进行选择。在管理和非管理磁盘方面,我更喜欢管理磁盘。这是因为在非管理磁盘中,我们选择创建存储帐户,而且由于我们为多个应用服务器创建它,每个应用服务器将有其单独的存储帐户。所有存储帐户可能都会落入单个存储单元,这可能导致单点故障。另一方面,在管理磁盘的情况下,Azure 通过将我们的磁盘存储在单独的存储单元中来管理我们的磁盘,这使其高度可用。

如果您不提供这些细节,系统将自动设置。

  1. 在下一个屏幕中,我们需要审查向导中定义的所有细节,如下截图所示:

  1. 在页面底部,您将找到一个链接,该链接将使您能够以模板形式或以不同语言的代码形式下载完整的配置。请参阅以下截图,显示了我们提供的配置生成的代码:

  1. 点击 确定 开始部署虚拟机。

现在,我们的仪表板应该在一段时间后运行一个 VM,如下截图所示:

现在您可以访问虚拟机,下载您的应用程序,并像在本地机器上一样部署它。

同样,我们可以为您的应用程序创建多个 VM 实例,作为应用服务器。

此外,我们可以创建一个带有 MongoDB 服务器安装的 VM。您需要遵循的安装步骤与我们在第四章中定义的步骤类似,交互式数据服务

我们可以通过单击仪表板上的 VM(即 appprod)图标来查看 VM 的性能,应该如下截图所示:

接下来,我们需要将之前创建的应用服务器添加到负载均衡器中。因此,我们需要按以下步骤创建负载均衡器:

单击“创建”按钮以启动 LB 创建。

  1. 一旦负载均衡器准备好供我们使用,我们应该能够看到以下屏幕,显示其详细信息:

  1. 接下来,我们需要添加后端池,即我们的应用服务器,如下截图所示:

  1. 现在我们需要添加健康探测,即您的应用程序的健康状态,如下所示:

接下来,我们将按照这里所示的方式为我们的应用程序添加前端池。

现在我们已经为我们的应用程序设置好了负载均衡器。

您可以在 Azure 文档的此链接中阅读有关负载均衡器的更多信息:docs.microsoft.com/en-us/azure/load-balancer/load-balancer-overview

现在,我们已经根据我们的架构图创建了基础设施。是时候为我们在 Azure 基础设施上部署应用程序配置 Jenkins 了。

使用 Jenkins 和 Azure 进行 CI/CD 流水线

首先,我们需要转到活动目录服务,您可以在下一个截图中看到:

现在我们需要注册我们的应用程序,因此,请在左窗格中选择“应用程序注册”。您将看到一个类似于下一个屏幕的屏幕,在那里您需要提供您的应用程序详细信息:

  1. 之后,您将能够生成访问 Jenkins 作业所需的密钥。

  2. 您将看到下一个屏幕,其中包含秘密密钥的详细信息,您还将在同一页上找到其他详细信息,例如“对象 ID”和“应用程序 ID”:

现在我们有了配置 Jenkins 作业所需的信息。因此,请转到 Jenkins 控制台,在“管理 Jenkins”部分中的“管理插件”中安装插件“Azure VM 代理”。

安装插件后,转到“管理 Jenkins”,单击“配置系统”,如下一个截图所示:

在下一个屏幕中,滚动到名为 Cloud 的底部部分,单击“添加云”按钮,并选择新的 Microsoft Azure VM 代理选项。这将在同一页上生成一个部分。

您可以在其文档中阅读有关 MS Azure VM 代理插件的更多信息(wiki.jenkins.io/display/JENKINS/Azure+VM+Agents+plugin)。

在最后一个屏幕中,您需要添加之前生成的 Azure 凭据。如果您单击下一个屏幕中可以看到的“添加”按钮,您可以添加诸如“订阅 ID”等值:

在同一部分的下一部分,您需要提供 VM 的详细配置,例如模板、VM 类型等:

在上面的截图中,标签是最重要的属性,我们将在 Jenkins 作业中使用它来识别该组。

现在,您需要提供您想要执行的操作,也就是说,如果您想要部署您的应用程序,您可以提供下载代码并运行应用程序的命令。

单击“保存”以应用设置。

现在,在 Jenkins 中创建一个新的作业。此外,在 GitBucket 部分,您通常提供存储库详细信息的地方,您将找到一个新的复选框,称为“限制此项目可以运行的位置”,并要求您提供标签表达式名称。在我们的情况下,它是msubuntu。就是这样!

现在我们已经准备好运行我们的 Jenkins 作业,将我们的应用程序部署到 VM(即应用服务器)上。

最后,我们能够在 Azure 平台上部署我们的应用程序。

总结

在本章中,您已经了解了由微软提供的 Azure 平台,并在其上部署了您的云原生应用程序。我们看了一种在 Azure 平台上构建相同基础设施的不同方法。您还看到了 Jenkins 与 Azure 平台的集成,用于 CI/CD。在下一章中,我们将看看一些非常有用的工具,用于管理和解决与应用程序相关的问题,并以更快的方式解决这些问题,以便我们的应用程序可以保持零停机时间。敬请关注下一章关于监控的内容!

第十三章:监控云应用程序

在前几章中,我们讨论了云原生应用程序开发,并将其部署到云平台供客户使用,以提高可用性。我们的工作还没有结束。基础设施和应用程序管理是一个完全独立的领域或流,它监控基础设施以及应用程序的性能,使用工具实现最小或零停机。在本章中,我们将讨论一些可以帮助您做到这一点的工具。

本章将涵盖以下主题:

  • AWS 服务,如 CloudWatch,Config 等

  • Azure 服务,如应用程序洞察、日志分析等

  • ELK 堆栈的日志分析简介

  • 开源监控工具,如 Prometheus 等

在云平台上进行监控

到目前为止,我们已经讨论了如何开发应用程序并在不同平台上部署它,以使其对客户业务模型有用。然而,即使在开发应用程序之后,您仍需要具有专业知识的人员,他们将利用工具在平台上管理您的应用程序,这可能是公共云或本地部署。

在本节中,我们将主要讨论公共云提供商提供的工具或服务,使用这些工具我们可以管理基础设施,并关注应用程序洞察,即性能。

在继续讨论工具之前,让我们在为任何应用程序分配基础设施时考虑一些要点:

  • 定期对一定的请求集合进行负载测试是一个好的做法。这将帮助您判断应用程序的初始资源需求。我们可以提到的一些工具是 Locust (locust.io/)和 JMeter (jmeter.apache.org/)。

  • 建议以最少的配置分配资源,并使用与应用程序使用情况相关的自动扩展工具。

  • 在资源分配方面应该尽量减少手动干预。

考虑所有前述要点。确保建立监控机制以跟踪资源分配和应用程序性能是必要的。让我们讨论云平台提供的服务。

基于 AWS 的服务

以下是AWS亚马逊云服务)提供的服务及其在应用程序和基础设施监控方面的使用。

云监控

这项 AWS 服务跟踪您的 AWS 资源使用情况,并根据定义的警报配置向您发送通知。可以跟踪 AWS 计费、Route 53、ELB 等资源。以下屏幕截图显示了一个触发的警报:

最初,我们必须在console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarm:alarmFilter=ANY设置 CloudWatch 警报。

您应该看到以下屏幕,在那里您需要单击“创建警报”按钮,根据一些指标创建自己的警报:

现在,单击“创建警报”按钮。将弹出一个向导,询问需要监控的指标:

上述屏幕截图列出了所有可监控的指标,以及可以设置警报的指标。

在下一个屏幕中,我们需要检查 EC2 指标。根据您的要求,您可以选择任何指标,例如,我们将选择 NetworkIn 指标并单击“下一步”:

在下一个屏幕上,我们需要提供警报名称和描述,以及警报预览。此外,我们需要根据触发警报的条件提供条件。

此外,我们需要设置服务通知服务,通知需要以电子邮件形式发送:

添加完详细信息后,单击“创建警报”按钮设置警报。

现在,每当“NetworkIn”指标达到其阈值时,它将通过电子邮件发送通知。

同样,我们可以设置不同的指标来监视资源利用率。

另一种创建警报的方法是在资源的监视部分选择“创建警报”按钮,如下截图所示:

您可以查看 AWS 文档(aws.amazon.com/documentation/cloudwatch/)获取更多信息。

CloudTrail

这是 AWS 云服务中最重要的之一,默认情况下会跟踪 AWS 账户上的任何活动,无论是通过控制台还是编程方式。在这项服务中,我们不需要配置任何内容。如果您的账户受到威胁,或者我们需要检查资源操作等情况,这项服务就是必需的。

以下截图将显示与账户相关的一些活动:

有关更多信息,您可以查看 AWS 文档(aws.amazon.com/documentation/cloudtrail/)。

AWS Config 服务

这是另一个 AWS 服务,我们可以根据定义的模板规则检查 AWS 资源的配置。

请注意,此服务将需要创建服务角色以访问 AWS 资源。

在这项服务中,我们只需要根据提供的模板设置规则。AWS 或客户模板用于对我们作为应用程序部署的一部分创建的资源进行检查。要向服务配置添加新规则,请转到console.aws.amazon.com/config/home?region=us-east-1#/rules/view

在上述屏幕中,我们需要添加一个新规则,该规则将评估所有资源或您指定的资源。单击“添加规则”以添加新规则,如下所示:

在上述截图中,选择规则以打开基于需要跟踪的资源的资源监视配置。

上述截图是 AWS ec2-instance-in-vpc 模板配置,它将帮助您验证 EC2 是否在具有正确配置的 VPC 中。在这里,您可以指定需要评估的 VPC。

单击“保存”以添加新规则。一旦评估完成,我们将看到以下屏幕:

以下资源报告显示如下:

您可以查看 AWS 文档(aws.amazon.com/documentation/config/)获取更多信息。

Microsoft Azure 服务

以下是 Microsoft Azure 提供的服务,可以帮助您管理应用程序性能。

应用程序洞察

这项由 Azure 提供的服务可帮助您管理应用程序性能,对于 Web 开发人员非常有用,可以帮助他们检测、诊断和诊断应用程序问题。

要设置应用程序洞察,您只需要知道应用程序和组名称,这些名称是您基础架构所在的。现在,如果您在左侧窗格上单击“+”号,您应该会看到类似以下截图的屏幕:

在这里,我们可以选择应用程序洞察服务,需要提供应用程序洞察名称、需要监视的组名称以及需要启动的区域。

一旦启动,您将看到以下屏幕,其中将向您展示如何使用应用程序洞察配置资源。以下是一些描述的指标:

查看完整的参考文档,访问docs.microsoft.com/en-us/azure/application-insights/app-insights-profiler,其中将提供有关如何配置应用程序洞察与资源的完整信息。

现在出现的问题是应用程序洞察监视哪些指标。以下是一些描述的指标:

  • 请求速率响应时间失败率:这可以让您了解请求的类型及其响应时间,有助于资源管理

  • Ajax 调用:这将跟踪网页的速率、响应时间和失败率。

  • 用户和会话详情:这跟踪用户和会话信息,如用户名、登录、注销详情等

  • 性能管理:这跟踪 CPU、网络和 RAM 的详细信息

  • 主机诊断:这是为了计算 Azure 的资源

  • 异常:这可以让您了解服务器和浏览器报告的异常

您可以为系统配置许多指标。有关更多信息,请查看docs.microsoft.com/en-us/azure/application-insights/app-insights-metrics-explorer

您可以查看 Azure 文档(docs.microsoft.com/en-us/azure/application-insights/),了解更多与应用程序洞察相关的信息。

到目前为止,我们一直在云平台上验证和监视应用程序及其基础设施。然而,一个非常重要的问题是:如果出现应用程序问题,我们该如何进行故障排除?下一部分关于 ELK 堆栈将帮助您确定问题,这可能是系统级或应用程序级的问题。

ELK 堆栈介绍

ELK 堆栈由 Elasticsearch、Logstash 和 Kibana 组成。所有这些组件一起工作,收集各种类型的日志,可以是系统级日志(即 Syslog、RSYSLOG 等)或应用程序级日志(即访问日志、错误日志等)。

有关 ELK 堆栈的设置,您可以参考这篇文章,其中除了 ELK 堆栈外,还使用 Filebeat 配置将日志发送到 Elasticsearch:

www.digitalocean.com/community/tutorials/how-to-install-elasticsearch-logstash-and-kibana-elk-stack-on-ubuntu-14-04

Logstash

Logstash 需要安装在需要收集日志并将其传送到 Elasticsearch 以创建索引的服务器上。

安装 Logstash 后,建议配置位于/etc/logstashlogstash.conf文件,包括 Logstash 日志文件的旋转(即/var/log/logstash/*.stdout*.err*.log)或后缀格式,如数据格式。以下代码块是供您参考的模板:

    # see "man logrotate" for details 

    # number of backlogs to keep 
    rotate 7 

    # create new (empty) log files after rotating old ones 
    create 

    # Define suffix format 
    dateformat -%Y%m%d-%s 

    # use date as a suffix of the rotated file 
    dateext 

   # uncomment this if you want your log files compressed 
   compress 

   # rotate if bigger that size 
   size 100M 

   # rotate logstash logs 
   /var/log/logstash/*.stdout 
   /var/log/logstash/*.err 
   /var/log/logstash/*.log { 
       rotate 7 
       size 100M 
       copytruncate 
       compress 
       delaycompress 
       missingok 
       notifempty 
    } 

为了将日志传送到 Elasticsearch,您需要在配置中有三个部分,名为输入、输出和过滤,这有助于创建索引。这些部分可以在单个文件中,也可以在单独的文件中。

Logstash 事件处理管道按照输入-过滤-输出的方式工作,每个部分都有自己的优势和用途,其中一些如下:

  • 输入:这个事件需要从日志文件中获取数据。一些常见的输入包括文件,它使用tailf读取文件;Syslog,它从监听端口514的 Syslogs 服务中读取;beats,它从 Filebeat 收集事件,等等。

  • 过滤器:Logstash 中的这些中间层设备根据定义的过滤器对数据执行某些操作,并分离符合条件的数据。其中一些是 GROK(根据定义的模式结构化和解析文本)、clone(通过添加或删除字段复制事件)等。

  • 输出:这是最终阶段,我们将经过过滤的数据传递到定义的输出。可以有多个输出位置,我们可以将数据传递到进一步索引。一些常用的输出包括 Elasticsearch(非常可靠;一个更容易、更方便的平台来保存您的数据,并且更容易在其上查询)和 graphite(用于以图表形式存储和显示数据的开源工具)。

以下是 Syslog 日志配置的示例:

  • Syslog 的输入部分写成如下形式:
   input { 
     file { 
     type => "syslog" 
    path => [ "/var/log/messages" ] 
    } 
   }

  • Syslog 的过滤器部分写成如下形式:
   filter { 
     grok { 
      match => { "message" => "%{COMBINEDAPACHELOG}" } 
     } 
    date { 
     match => [ "timestamp" , "dd/MMM/yyyy:HH:mm:ss Z" ] 
    } 
  } 

  • Syslog 的输出部分写成如下形式:
   output { 
     elasticsearch { 
       protocol => "http" 
       host => "es.appliedcode.in" 
       port => "443" 
       ssl => "true" 
       ssl_certificate_verification => "false" 
       index => "syslog-%{+YYYY.MM.dd}" 
       flush_size => 100 
      } 
   } 

用于传输日志的配置文件通常存储在/etc/logstash/confd/中。

如果为每个部分创建单独的文件,则需要遵循命名文件的约定;例如,输入文件应命名为10-syslog-input.conf,过滤器文件应命名为20-syslog-filter.conf。同样,对于输出,它将是30-syslog-output.conf

如果要验证配置是否正确,可以执行以下命令:

 $ sudo service logstash configtest

有关 Logstash 配置的更多信息,请参阅文档示例www.elastic.co/guide/en/logstash/current/config-examples.html

Elasticsearch

Elasticsearch (www.elastic.co/products/elasticsearch)是一个日志分析工具,它帮助存储并根据配置和时间戳创建索引,解决了开发人员试图识别与其问题相关的日志的问题。Elasticsearch 是基于 Lucene 搜索引擎的 NoSQL 数据库。

安装完 Elasticsearch 后,您可以通过点击以下 URL 验证版本和集群详细信息: http://ip-address:9200/

输出将如下所示:

这证明了 Elasticsearch 正在运行。现在,如果要查看日志是否已创建,可以使用以下 URL 查询 Elasticsearch:

http://ip-address:9200/_search?pretty

输出将如下屏幕截图所示:

要查看已创建的索引,可以点击以下 URL:

http://ip-address:9200/_cat/indices?v

输出将类似于以下屏幕截图:

如果您想了解更多关于 Elasticsearch 查询、索引操作等内容,请阅读本文:

www.elastic.co/guide/en/elasticsearch/reference/current/indices.html

Kibana

Kibana 工作在 Elasticsearch 的顶层,可视化提供环境接收的数据的洞察,并帮助做出必要的决策。简而言之,Kibana 是一个用于从 Elasticsearch 搜索日志的 GUI。

安装 Kibana 后,应出现在http://ip-address:5601/,它将要求您创建索引并配置 Kibana 仪表板:

配置完成后,应出现以下屏幕,其中显示了带有时间戳的日志格式:

现在,我们需要创建仪表板,以便以图表、饼图等形式查看日志。

有关创建 Kibana 仪表板的更多信息,请参阅 Kibana 文档(www.elastic.co/guide/en/kibana/current/dashboard-getting-started.html)。

作为 Kibana 的替代方案,您可能对 Grafana(grafana.com/)感兴趣,这也是一种分析和监控工具。

现在,问题是:Grafana 与 Kibana 有何不同?以下是答案:

Grafana Kibana
Grafana 仪表板专注于基于系统指标 CPU 或 RAM 的时间序列图表。Kibana 专用于日志分析。
Grafana 的内置 RBA(基于角色的访问)决定用户对仪表板的访问权限。 Kibana 无法控制仪表板访问权限。
Grafana 支持除 Elasticsearch 之外的不同数据源,如 Graphite、InfluxDB 等。 Kibana 与 ELK 堆栈集成,使其用户友好。

这是关于 ELK 堆栈的,它为我们提供了有关应用程序的见解,并帮助我们解决应用程序和服务器问题。在下一节中,我们将讨论一个名为Prometheus的本地开源工具,它对监视不同服务器的活动非常有用。

开源监控工具

在本节中,我们将主要讨论由第三方拥有并收集服务器指标以排除应用程序问题的工具。

Prometheus

Prometheus(prometheus.io)是一个开源监控解决方案,可跟踪系统活动指标,并在需要您采取任何操作时立即向您发出警报。这个工具是用Golang编写的。

这个工具类似于 Nagios 等工具正在变得越来越受欢迎。它收集服务器的指标,但也根据您的需求为您提供模板指标,例如http_request_duration_microseconds,以便您可以使用 UI 生成图表以更好地理解它并以高效的方式监视它。

请注意,默认情况下,Prometheus 在9090端口上运行。

要安装 Prometheus,请按照官方网站上提供的说明进行操作(prometheus.io/docs/introduction/getting_started/)。安装完成并且服务启动后,尝试打开http://ip-address:9090/status以了解状态。以下屏幕显示了 Prometheus 的构建信息,即版本、修订版本等。

要了解配置了哪些目标,请使用http://ip-address:9090/targets。输出将类似于这样:

为了生成图表,请使用http://ip-address:9090/graph并选择需要实现图表的指标。输出应类似于以下屏幕截图:

同样,我们可以请求由 Prometheus 识别的其他一些指标,例如主机上线状态。以下屏幕截图显示了一段时间内的主机状态:

Prometheus 有一些不同用途的组件,如下所示:

  • AlertManager:此组件将帮助您基于指标设置服务器的警报,并定义其阈值。我们需要在服务器中添加配置来设置警报。查看prometheus.io/docs/alerting/alertmanager/上的 AlertManager 文档。

  • Node exporter:此导出器对硬件和操作系统指标非常有用。在prometheus.io/docs/instrumenting/exporters/上阅读有关不同类型导出器的更多信息。

  • Pushgateway:此 Pushgateway 允许您运行批处理作业以公开您的指标。

  • Grafana:Prometheus 与 Grafana 集成,帮助仪表板查询 Prometheus 上的指标。

总结

这一章以不同的方式非常有趣。从基于云平台的工具,如 Cloudwatch 和 Application Insights 开始,这些工具帮助您在云平台上管理应用程序。然后,它转向开源工具,开发人员一直以来都将其作为首选,因为他们可以根据自己的需求进行定制。我们看了 ELK 堆栈,它一直很受欢迎,并且在许多组织中以某种方式经常被使用。

现在,我们已经到达了本书的结尾,但希望会有另一版,届时我们将讨论高级应用开发,并提供更多对 QA 受众有用的测试案例。尽情编码吧!

posted @ 2024-05-20 16:52  绝不原创的飞龙  阅读(27)  评论(0编辑  收藏  举报