AWS-高效-DevOps-第二版-全-

AWS 高效 DevOps 第二版(全)

原文:annas-archive.org/md5/1d4dd166d756e1a5e6004c11dd27409c

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

DevOps 运动已经改变了现代科技公司工作的方式。Amazon Web Services(AWS)一直处于云计算革命的前沿,也是 DevOps 运动的关键推动者,创造了大量的托管服务,帮助你实施 DevOps 原则。

本书将帮助你了解最成功的科技初创公司如何在 AWS 上启动和扩展他们的服务,并学习如何做到这一点。本书解释了如何将基础设施视为代码,这意味着你可以像控制软件一样轻松地将资源上线和下线。你还将构建一个持续集成和持续部署的管道,以保持你的应用程序始终最新。

一旦你掌握了这些,你将学习如何扩展应用程序,以便在流量激增时也能为用户提供最佳性能,利用最新的技术,如容器。此外,你还将深入了解监控和告警,确保用户在使用你的服务时拥有最佳体验。在最后的章节中,你将介绍 AWS 内置工具,如 CodeDeploy 和 CloudFormation,这些工具被许多 AWS 管理员用于执行 DevOps。

到本书结尾时,你将学会如何使用最新和最重要的 AWS 工具来确保平台和数据的安全。

本书适合谁阅读

如果你是开发人员、DevOps 工程师,或者你在一个希望构建并使用 AWS 进行软件基础设施的团队中工作,那么这本书适合你。为了充分理解本书内容,需要具备基本的计算机科学知识。

本书涵盖内容

第一章,云与 DevOps 革命,本章将为任何人打下 DevOps 和云计算旅程的基础。深入理解 DevOps 文化、DevOps 术语和 AWS 生态系统,为未来章节的路线图做准备。

第二章,部署你的第一个 Web 应用程序,本章将展示最简单形式的 AWS 基础设施配置,并介绍一些最佳的 AWS 身份验证实践。我们将创建一个简单的 Web 应用程序,了解如何在 AWS 上托管该应用程序的最简单方式,最后将终止实例。整个过程将通过 AWS CLI 工具实现,并将在后续章节中进行自动化,帮助理解如何使用不同的 AWS 和其他著名服务与产品自动化手动任务。

第三章,将基础设施视为代码,本章将重点介绍使用 AWS 原生工具 CloudFormation 进行自动化资源配置,以及创建 CloudFormation 模板的技巧。然后,我们将介绍一个配置管理系统,使用 Ansible 自动化应用部署。

第四章,使用 Terraform 进行基础设施即代码,本章将重点介绍 Terraform 的基本原理。我们将使用 Terraform 模板为 AWS 实例配置资源,然后扩展 Terraform 的功能,通过另一个 Terraform 模板来部署应用程序。最后,我们将通过将 Terraform 与 Ansible 自动化结合来理解 AWS 的资源配置。

第五章,增加持续集成和持续部署,本章将重点介绍如何使用 AWS DevOps 服务和自动化测试框架构建 CI/CD 管道。我们将使用多个工具来准备技术框架,如版本控制、持续集成、自动化测试工具、AWS 原生 DevOps 工具以及基础设施自动化工具,以帮助我们理解如何通过“快速失败”和“频繁失败”来构建一个健壮的生产环境。

第六章,扩展您的基础设施,本章将介绍其他有用的、具有成本效益的 AWS 服务,用于构建具有性能导向的可扩展 AWS 基础设施。AWS 服务,如 Elastic Cache、CloudFront、SQS、Kinesis 等,将用于构建我们的应用框架。

第七章,在 AWS 中运行容器,本章将介绍市场上最为小众的技术之一——Docker。我们将从理解容器开始,学习使用 Docker 的所有基础概念。在这里,我们将使用 ECS 构建 AWS 容器环境,并为我们的应用构建一个完整的 ECS 框架。最后,我们将使用 AWS DevOps 工具集构建一个完整的 CI/CD 管道来部署 AWS ECS 服务。

第八章,强化 AWS 环境的安全性,本章将重点介绍如何使用 AWS 审计服务、AWS IAM 服务来管理并根据角色提供有限访问权限、加强 AWS VPC 模型,最后保护环境免受勒索软件和其他漏洞的攻击。

第九章,监控与警报,本章将重点介绍如何使用 AWS CloudWatch 服务为您的 AWS 环境构建监控框架。我们将使用一些著名的仪表盘工具来可视化日志。最后,将使用 AWS SNS 服务创建通知框架,以便在 AWS 环境出现问题时通知用户并采取纠正措施。有关此章节的详细信息,请参考www.packtpub.com/sites/default/files/downloads/Monitoring_and_Alerting.pdf

如何充分利用本书

本书所需的软件如下:

  • AWS 管理控制台

  • AWS 计算服务

  • AWS IAM

  • AWS CLI 设置

  • 用于 Web 应用程序的 JavaScript

下载示例代码文件

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

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

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

  2. 选择“SUPPORT”标签。

  3. 点击“Code Downloads & Errata”。

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

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

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Effective-DevOps-with-AWS-Second-Edition。如果代码有更新,将会在现有的 GitHub 仓库中更新。

我们还提供了来自我们丰富图书和视频目录中的其他代码包,地址为 github.com/PacktPublishing/。快来看看吧!

这些代码也可以在以下仓库中找到:

下载彩色图像

我们还提供了一份 PDF 文件,里面有本书中使用的截图/图表的彩色图像。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789539974_ColorImages.pdf

使用的约定

本书中使用了若干文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入以及 Twitter 用户名。以下是一个示例:“点击 start 按钮并搜索 settings 选项。”

代码块的设置方式如下:

var http = require("http") http.createServer(function (request, response) {
// Send the HTTP header
// HTTP Status: 200 : OK
// Content Type: text/plain
response.writeHead(200, {'Content-Type': 'text/plain'})
// Send the response body as "Hello World" response.end('Hello World\n')
}).listen(3000)

// Console will print the message console.log('Server running')

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

$ aws ec2 describe-instance-status --instance-ids i-057e8deb1a4c3f35d --output text| grep -i SystemStatus
SYSTEMSTATUS ok

任何命令行输入或输出会按照以下方式书写:

$ aws ec2 authorize-security-group-ingress \
   --group-name HelloWorld \
   --protocol tcp \
   --port 3000 \
   --cidr 0.0.0.0/0

粗体:表示一个新术语、重要单词或在屏幕上看到的单词。例如,菜单或对话框中的单词会以这种形式出现在文本中。以下是一个示例:“在此菜单中,找到名为 Windows Subsystem for Linux (Beta) 的功能。”

警告或重要说明如下所示。

提示和技巧如下所示。

获取联系

我们欢迎读者的反馈。

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

勘误:尽管我们已尽力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现任何错误,我们将非常感激您向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并填写相关信息。

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

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

评价

请留下评价。阅读并使用本书后,为什么不在您购买本书的网站上留下评价呢?潜在读者可以查看并参考您的公正意见做出购买决策,我们 Packt 也能了解您对我们产品的看法,作者也能看到您对其书籍的反馈。感谢您!

有关 Packt 的更多信息,请访问 packt.com

第一章:云计算与 DevOps 革命

科技行业正在不断变化。尽管互联网仅在二十多年前诞生,但它已经彻底改变了我们的生活方式。每天,有超过十亿人访问 Facebook;每分钟,大约有 300 小时的视频内容被上传到 YouTube;每秒,Google 处理约 40,000 个搜索查询。能够处理如此庞大的规模并不容易。然而,本书将为您提供有关部署哲学、工具及采用最佳实践的实用指南。通过使用Amazon Web Services (AWS),您将能够构建管理和扩展基础设施、工程流程和应用程序所需的关键元素,且花费和精力最小。第一章将介绍以下主题的新范式:

  • 从云的角度思考,而非从基础设施的角度

  • 采用 DevOps 文化

  • 在 AWS 中部署

从云的角度思考,而非从基础设施的角度

现在我们将描述一个发生在 2011 年 12 月底的数据中心实际事件,当时我们的实时监控系统收到了数十个警报。这是由于与数据中心的连接丧失所导致的。管理员立即赶往网络操作中心 (NOC),希望这只是监控系统中的一个小故障。由于有如此多的冗余,我们可能会疑惑一切是如何离线的。可惜的是,NOC 房间的大监控屏幕全部变成了红色,这可不是个好兆头。这是一个非常漫长噩梦的开始。

事实上,这次事故是由一名电工引发的,他在数据中心工作时不小心触发了火警警报。在发生此事件的几秒钟内,灭火系统启动,并将氩气释放到服务器机架上。不幸的是,这种灭火系统在释放气体时发出的噪音太大,声波瞬间摧毁了数百个硬盘,导致数据中心设施彻底停运。恢复过程花费了几个月的时间。

部署自有硬件与云端部署

不久前,大小科技公司都需要有一支合适的技术运营团队,能够搭建基础设施。这个过程大致是这样的:

  1. 飞往您希望设置基础设施的地点。在这里,参观不同的数据中心及其设施。观察地板考虑因素、电力考量、暖通空调 (HVAC)、防火系统、物理安全等。

  2. 选择互联网服务提供商。最终,您需要考虑服务器和更多带宽,但过程是一样的——您希望为您的服务器获取互联网连接。

  3. 一旦完成这一步,就到了购买硬件的时刻。做出正确的决策非常重要,因为你可能会花费公司大部分的资金来选择和购买服务器、交换机、路由器、防火墙、存储、UPS(用于停电时)、KVM、网络电缆、标签(每个系统管理员都重视这一点),以及一堆备件、硬盘、RAID 控制器、内存、电源线等。

  4. 到了这一步,一旦硬件购买并运送到数据中心,你就可以将所有设备安装好,连接所有服务器,并启动所有设备。你的网络团队可以开始工作,通过不同的链路为新数据中心建立连接,配置边缘路由器、交换机、机架顶部交换机、KVM 和防火墙(有时)。接下来是存储团队,他们将提供急需的 网络附加存储 (NAS) 或 存储区域网络 (SAN)。然后是你的系统运维团队,他们将给服务器制作镜像、升级 BIOS(有时)、配置硬件 RAID,最后安装操作系统。

这不仅是一个大团队的全职工作,而且甚至在达到这个目标之前,你还需要花费大量的时间和金钱。正如你在本书中看到的,使用 AWS 启动新的服务器只需要几分钟。实际上,你很快会看到如何在几分钟内部署和运行多个服务,并且仅在需要时使用,采用 按需付费 模式。

成本分析

从成本角度来看,在像 AWS 这样的云基础设施中部署服务和应用程序通常比购买自己的硬件便宜得多。如果你选择部署自己的硬件,你必须提前支付所有前面提到的硬件费用(服务器、网络设备、存储等),并且在某些情况下,还需要支付许可软件费用。而在云环境中,按需付费。你可以迅速添加或移除服务器,且只需为服务器运行的时长付费。此外,如果你利用 PaaS 和 SaaS 应用程序,你通常会通过降低运营成本节省更多钱,因为你不需要那么多管理员来管理服务器、数据库、存储等。大多数云服务提供商(包括 AWS)还提供分层定价和量大优惠。随着服务的增长,你在每个存储单元、带宽等方面的费用会逐渐减少。

按需基础设施

如你所见,在云中部署时,你只需为所提供的资源付费。大多数云公司利用这一点,随时根据网站流量的变化调整基础设施的规模。能够随时按需添加或移除新服务器和服务,正是一个高效云基础设施的主要优势之一。

在以下示例中,您可以看到在 11 月期间,www.amazon.com/ 的流量情况。由于黑色星期五和网络星期一,月底时流量增加了三倍:

如果公司以传统方式托管其服务,它们将需要预留足够的服务器来处理这种流量,以便在这个月中,平均只有 24%的基础设施会被使用:

然而,由于能够动态扩展,它们仅能提供实际需要的资源,然后动态吸收黑色星期五和网络星期一所带来的流量激增:

您还可以看到,定期在多个组织中使用云服务时,快速自动扩展能力所带来的好处。这又是公司medium的一个实际案例,非常频繁。在这里,故事会迅速传播,流量大幅变化。2015 年 1 月 21 日,白宫在总统奥巴马开始演讲前发布了国情咨文的文字记录:http😕/bit.ly/2sDvseP。正如您在下图中所看到的,得益于云端和自动扩展能力,平台能够通过将前端服务使用的服务器数量翻倍,吸收公告发布时瞬间的五倍流量激增。随后,当流量自然减少时,您可以自动从您的服务器群中移除一些主机:

云的不同层次

云计算通常分为三种不同类型的服务,通常被称为服务模型,如下所示:

  • 基础设施即服务IaaS):这是构建云服务的一项基本构件,云中一切的基础都建立在此之上。IaaS 通常是一个虚拟化环境中的计算资源,提供处理能力、内存、存储和网络的组合。最常见的 IaaS 实体是虚拟机VMs)和网络设备,如负载均衡器或虚拟以太网接口,以及存储设备,如块存储。该层级非常接近硬件,提供了与将软件部署在云外时相同的灵活性。如果您有数据中心的经验,您会发现这层也大多适用于此。

  • 平台即服务 (PaaS):这一层是云计算中真正有趣的地方。当构建一个应用程序时,你可能需要一些常见的组件,例如数据存储和队列。PaaS 层提供了许多现成的应用程序,帮助你构建自己的服务,而无需担心管理和操作第三方服务,如数据库服务器。

  • 软件即服务 (SaaS):这一层是锦上添花。与 PaaS 层类似,你可以访问托管服务,但这一次,这些服务是完整的解决方案,专门用于特定目的,如管理或监控工具。

我们建议你阅读 国家标准与技术研究院(NIST)云计算定义,链接地址:nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-145.pdf,以及 NIST 云计算标准路线图,链接地址:www.nist.gov/sites/default/files/documents/itl/cloud/NIST_SP-500-291_Version-2_2013_June18_FINAL.pdf. 这本书涵盖了 PaaS 和 SaaS 类型的多个服务。在构建应用程序时,依赖这些服务与传统的云外环境相比,能带来显著差异。部署或迁移到新基础设施时,另一个成功的关键因素是采纳 DevOps 思维方式。

采用 DevOps 文化

拥有 DevOps 文化的公司运营完全依赖于采纳正确的文化,使得开发人员和运维团队能够协同工作。DevOps 文化提倡实施几种工程最佳实践,并依赖于你将在本书中了解的工具和技术。

DevOps 的起源

DevOps 是一个新的运动,正式开始于 2009 年的比利时,当时一群人在第一次由 Patrick Debois 组织的 DevOpsdays 会议上聚集,讨论如何将一些敏捷概念应用于基础设施。敏捷方法学改变了软件开发的方式。在传统的瀑布模型中,产品团队会提出规格;设计团队随后创建并定义用户体验和用户界面;工程团队会开始实施请求的产品或功能,然后将代码交给 QA 团队,QA 团队测试并确保代码根据设计规格正确运行。一旦修复了所有的错误,发布团队会将最终的代码打包,并交给技术运维团队,后者负责部署代码并随时间监控服务:

随着某些软件和技术开发复杂性的增加,传统的瀑布式流程暴露出了一些局限性。敏捷转型解决了其中一些问题,使设计师、开发人员和测试人员之间的互动更加频繁。这一变化提高了产品的整体质量,因为这些团队现在有更多的机会对产品开发进行迭代。然而,除了这一点之外,您仍然处于一个非常经典的瀑布式流程中,如下所示:

由这一新流程所增加的敏捷性并没有超出 QA 周期,是时候对软件开发生命周期的这一方面进行现代化了。敏捷流程的这一基础性变革,使设计师、开发人员和 QA 团队之间的合作更加紧密,这正是 DevOps 最初的目标,但很快,DevOps 运动开始重新思考开发人员和运维团队如何能够共同工作。

开发人员与运维团队的困境

在非 DevOps 文化中,开发人员负责开发新产品和功能,并维护现有代码,但最终,他们只有在代码发布时才能获得奖励。激励措施是尽可能快速地交付。另一方面,运维团队通常负责维护生产环境的正常运行。对于这些团队而言,变更是一件负面的事情。新特性和服务增加了停机的风险,因此,谨慎行动非常重要。为了最大限度地降低停机风险,运维团队通常需要提前安排任何部署,以便他们可以预先部署和测试生产部署,最大化成功的机会。企业软件公司通常会安排维护窗口,在这些情况下,生产变更每季度、半年或每年只能进行几次。不幸的是,许多时候,部署并不会成功,而且这背后有很多可能的原因。

一次性更改过多代码

变更的规模与引入关键缺陷的风险之间存在一定的相关性,如下所示:

生产环境的差异

许多时候,开发人员编写的代码在开发环境中工作良好,但在生产环境中却不行。很多时候,这是因为生产环境与其他环境差异很大,可能会出现一些无法预见的错误。常见的错误往往发生在开发环境中,因为服务通常在同一台服务器上共存,或者安全性水平不同。因此,服务在开发环境中可以互相通信,但在生产环境中却不能。另一个问题是开发环境可能运行的是某个库/软件的不同版本,因此与其通信的接口可能不同。开发环境中可能运行的是某个服务的更新版本,包含了生产环境中尚未出现的新功能;或者,这只是一个规模的问题。也许开发环境中使用的数据集没有生产环境中的数据集那么大,一旦新代码投入生产,扩展性问题就会显现出来。

沟通

信息技术中的一个最大困境是沟通不畅。

以下是根据 Conway 法则:

“设计系统的组织必然会受到这些组织的沟通结构的限制,从而生产出这些沟通结构的复制品。”

—梅尔文·康威

换句话说,你所构建的产品反映了你组织的沟通方式。很多时候,问题并不来自于技术,而是来自于围绕技术的人员和组织。如果你的开发团队和运维团队之间存在功能失调,这种问题会显现出来。在 DevOps 文化中,开发人员和运维人员有着不同的思维方式。他们通过共享责任、采用相似的方法论来打破围绕这些团队的隔阂,从而提高生产力。他们共同努力,尽可能地自动化所有可能的工作(并非一切都能一次性自动化),并使用指标来衡量成功。

DevOps 文化的关键特征

正如我们所提到的,DevOps 文化依赖于一些原则。这些原则包括对一切进行源代码控制(版本控制)、自动化所有可能的工作,并衡量一切。

一切都进行源代码控制

版本控制软件已经存在了数十年,但通常只有产品代码被检查。在实践 DevOps 时,不仅应用代码需要检查,配置、测试、文档以及部署应用所需的所有基础设施自动化也需要检查。一切都经过 源代码管理器 (SCM) 的常规审查过程。

自动化测试

自动化软件测试早于 DevOps 的历史,但它是一个很好的起点。开发人员常常专注于实现功能,却忘记为代码添加测试。在 DevOps 环境中,开发人员负责为其代码添加适当的测试。QA 团队仍然可以存在;然而,像其他工程团队一样,他们的工作是围绕测试构建自动化。

这个话题可以写成一本书,但简而言之,在开发代码时,要记住有四个层级的测试自动化需要关注,以便成功实施 DevOps:

  • 单元测试:这是测试每个代码块和函数的功能。

  • 集成测试:这是确保服务和组件能够协同工作。

  • 用户界面测试:这通常是最具挑战性的组件,成功实现起来最为困难。

  • 系统测试:这是端到端的测试。例如,在一个照片分享应用程序中,端到端测试可能是打开主页、登录、上传照片、添加标题、发布照片,然后注销。

自动化基础设施的供应和配置

在过去几十年里,平均基础设施的规模和堆栈的复杂性迅速增长。以前能够通过临时管理基础设施的方式,现在已经非常容易出错。在 DevOps 文化中,服务器、网络和服务的供应和配置通常通过自动化来完成。配置管理通常是 DevOps 运动的代名词。然而,正如你所知,这只是一个大拼图中的小部分。

自动化部署

正如你现在所知道的,按小块编写软件并尽早部署这些新块,以确保它们正常工作,变得更加容易。为了实现这一点,实践 DevOps 的公司依赖于持续集成和持续部署管道。每当新的代码块准备就绪时,持续集成管道便会启动。通过自动化测试系统,新代码会通过所有相关的可用测试。如果新代码没有明显的回归问题,它就被视为有效,并可以合并到主代码库中。到那时,在开发人员无需进一步参与的情况下,包含这些新更改的新版本服务(或应用程序)将被创建,并交给一个称为持续部署系统的系统。持续部署系统将接受这些新构建,并将其自动部署到不同的可用环境中。根据部署管道的复杂性,这可能包括预发布环境、集成环境,有时还包括预生产环境。最终,如果一切按计划进行(没有任何手动干预),这个新版本将被部署到生产环境中。

关于实践持续集成和持续部署的一个常见误解是,新的功能不需要在开发完成后立即向用户开放。在这种模式下,开发人员通常依赖功能标记和暗启动。基本上,每当你开发新的代码并希望隐藏它时,你会在服务配置中设置一个标记,描述谁能访问新功能以及如何访问。在工程层面,通过这种方式进行暗启动,你可以将生产流量发送到服务,但在 UI 中隐藏它,以查看它对数据库或性能的影响。例如,在产品层面,你可以决定只对一小部分用户启用新功能,以观察新功能是否正常工作,以及使用新功能的用户是否比对照组用户更活跃。

衡量一切

衡量一切是 DevOps 驱动的公司采用的最后一个主要原则。正如爱德华兹·戴明所说,你无法改进无法衡量的东西。 DevOps 是一个不断发展的过程和方法论,通过这些指标来评估和改进产品及团队的整体质量。从工具和操作的角度来看,以下是大多数组织关注的一些指标:

  • 每天推送到生产环境的构建次数

  • 生产环境中回滚的频率(当你的测试未能发现一个重要问题时,通常会出现这种情况)

  • 代码覆盖率的百分比

  • 触发值班工程师立即关注的警报频率

  • 故障发生的频率

  • 应用性能

  • 平均修复时间MTTR),即故障或性能问题修复的速度

在组织层面,衡量转向 DevOps 文化的影响也很有意义。虽然这很难衡量,但你可以考虑以下几点:

  • 团队之间的协作程度

  • 团队自主性

  • 跨职能的工作和团队协作

  • 产品的流动性

  • 开发与运维之间的沟通频率

  • 工程师的幸福感

  • 对自动化的态度

  • 对指标的痴迷

正如你刚刚所学到的,拥有 DevOps 文化意味着首先要改变传统的思维方式,即开发和运维是两个独立的部门,并且让团队在软件开发生命周期的各个阶段进行更多的合作。

除了新的思维方式外,DevOps 文化还需要一套特定的工具,专注于自动化、部署和监控:

使用 AWS,亚马逊提供了许多 PaaS 和 SaaS 类型的服务,能帮助我们实现这一目标。

在 AWS 上部署

AWS 处于云服务提供商的前沿。自 2006 年推出 SQS 和 EC2 后,亚马逊迅速成为最大的 IaaS 提供商。它们拥有最大的基础设施和生态系统,并且不断增加新的功能和服务。2018 年,它们的活跃客户数超过了百万。在过去几年里,AWS 成功改变了人们对云计算的认知,现在部署新服务已经成为常态。使用 AWS 的托管工具和服务是大幅提升生产力、保持团队精简的方式。亚马逊不断倾听客户反馈并关注市场趋势。因此,当 DevOps 运动开始普及时,亚马逊发布了多项专门为实施 DevOps 最佳实践而设计的新服务。本书将展示这些服务如何与 DevOps 文化协同作用。

如何利用 AWS 生态系统?

亚马逊的服务就像乐高积木。如果你能想象最终产品的样子,那么就可以探索不同的服务并将它们组合在一起,从而构建出快速、高效搭建产品所需的技术栈。当然,在这种情况下,如果是一个很大的假设,且与乐高不同,理解每个服务的功能远不如乐高积木那样直观和色彩丰富。因此,本书以非常实用的方式编写;在各章节中,我们将以一个 Web 应用为例,像对待核心产品一样进行部署。你将看到如何扩展支撑它的基础设施,使得数百万用户能够使用,并且让它更安全。当然,我们会按照 DevOps 最佳实践进行操作。通过这一过程,你将了解 AWS 提供的多种托管服务和系统,帮助完成一系列常见任务,如计算、网络、负载均衡、数据存储、监控、程序化管理基础设施和部署、缓存以及排队等。

AWS 如何与 DevOps 文化协同?

正如你在本章之前看到的,DevOps 文化的核心是重新思考工程团队如何协作,通过打破开发与运维之间的壁垒,并引入一组新工具,以便实施最佳实践。AWS 在这方面提供了许多不同的支持。对于一些开发者而言,运维世界可能是令人害怕且让人困惑的,但如果想要更好的工程师合作,就必须将服务运行的各个方面暴露给整个工程团队。

作为运维工程师,你不能对开发人员抱有守门员的心态。相反,更好的做法是通过让他们接触生产环境、参与平台各个组件的工作来让他们感到舒适。一个很好的起步方式是在 AWS 控制台中进行操作,如下所示:

尽管有些让人感到不知所措,但对于不熟悉浏览该 Web 界面的人来说,这仍然是比不断参考过时文档、使用 SSH 和随机操作来发现服务拓扑和配置的体验要好得多。当然,随着你技术的提升和应用的复杂化,操作的需求也会增加,Web 界面开始暴露出一些弱点。为了解决这个问题,AWS 提供了一个非常适合 DevOps 的替代方案。通过命令行工具和多个 SDK(包括 Java、JavaScript、Python、.NET、PHP、Ruby、Go 和 C++),可以访问 API。这些 SDK 让你能够管理和使用托管服务。最后,正如你在前一部分看到的,AWS 提供了许多符合 DevOps 方法论的服务,最终将使我们能够在短时间内实现复杂的解决方案。

你将在计算层面使用的主要服务之一是 Amazon 弹性计算云EC2),这是用于创建虚拟服务器的服务。之后,当你开始考虑如何扩展基础设施时,你会发现 Amazon EC2 自动扩展,这是一项允许你扩展 EC2 实例池的服务,以应对流量激增和主机故障。你还将通过 Amazon 弹性容器 服务ECS)探索容器的概念。除此之外,你将使用 AWS Elastic Beanstalk 创建和部署应用程序,这样你可以完全控制提供应用程序所需的 AWS 资源;你可以随时访问底层资源。最后,你将通过 AWS Lambda 创建无服务器函数,运行自定义代码,而无需将其托管在我们的服务器上。为了实现持续集成和持续部署系统,你将依赖以下四项服务:

  • AWS 简单存储服务S3):这是一个对象存储服务,可以让我们存储工件

  • AWS CodeBuild: 这将帮助我们测试我们的代码

  • AWS CodeDeploy: 这将帮助我们将工件部署到我们的 EC2 实例上

  • AWS CodePipeline: 这将帮助我们协调如何构建、测试和部署代码到不同的环境中

为了监控和衡量一切,你将依赖 AWS CloudWatch,稍后还会依赖 ElasticSearch/Kibana 来收集、索引和可视化度量数据和日志。为了将一些数据流式传输到这些服务,你将依赖 AWS Kinesis。为了发送电子邮件和短信警报,你将使用 Amazon SNS 服务。对于基础设施管理,你将大力依赖 AWS CloudFormation,它提供了创建基础设施模板的能力。最终,当你探索如何更好地保护我们的基础设施时,你将遇到 Amazon InspectorAWS Trusted Advisor,并将更详细地了解 IAM 和 VPC 服务。

总结

在本章中,你学到的内容是,采用 DevOps 文化意味着改变传统工程和运维团队的工作方式。与两个孤立的、目标和职责相对立的团队不同,拥有 DevOps 文化的公司通过互补的专业领域来更好地合作,采用新一套流程和工具。这些新流程和工具不仅包括自动化尽可能多的内容,从测试和部署到基础设施管理,还包括对所有内容进行衡量,以便你能随着时间的推移不断改进每个流程。说到云服务,AWS 在云服务目录中处于领先地位,其服务数量超过其他任何云服务提供商。所有这些服务都可以通过 API 和 SDK 使用,这对自动化非常有利。此外,AWS 为 DevOps 文化的每个关键特征提供了工具和服务。

在第二章《部署你的第一个 Web 应用》中,我们将终于动手并开始使用 AWS。本章的最终目标是拥有一个 Hello World 应用,任何人都可以通过互联网访问。

问题

  1. 什么是 DevOps?

  2. 什么是 DevOps – IaC?

  3. 列出 DevOps 文化的关键特征。

  4. 云中三个主要的服务模型是什么?

  5. 什么是 AWS 云?

深入阅读

你可以在 aws.amazon.com/products/ 上了解更多有关 AWS 服务的信息。

第二章:部署你的第一个 Web 应用程序

在上一章中,我们介绍了云计算的一般概念、它的好处以及什么是 DevOps 思想。AWS 提供了许多服务,所有服务都可以通过 Web 界面、命令行界面、各种 SDK 和 API 轻松访问。在本章中,我们将利用 Web 界面和命令行界面来创建和配置账户,并创建一个 Web 服务器来托管一个简单的 Hello World 应用程序,所有操作都将在几分钟内完成。

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

  • 创建和配置你的账户

  • 启动你的第一个 Web 服务器

技术要求

本章中使用的技术和服务如下:

  • AWS 管理控制台

  • AWS 计算服务

  • AWS IAM

  • AWS CLI 设置

  • Web 应用程序的 JavaScript

  • GitHub 上的现成代码

代码的 GitHub 链接如下:

创建和配置你的账户

如果你还没有注册 AWS,现在是时候注册了。

注册

这一步当然是相当简单且不言自明的。为了注册(如果你还没有注册),在浏览器中打开portal.aws.amazon.com,点击“创建一个新的 AWS 账户”按钮,并按照步骤进行操作。你将需要一个电子邮件地址和信用卡信息。

这个过程的两个例外如下:

  • 如果你打算在中国部署服务器,那么你需要在 AWS 中国区域创建账户,网址为www.amazonaws.cn/

  • AWS 为美国联邦、州和地方政府机构的特定监管需求提供了一项名为GovCloud的特殊服务。要注册此服务,请访问以下链接:aws.amazon.com/govcloud-us/contact/

在本书中,我们将使用位于弗吉尼亚州北部的服务器,因此你需要通过标准注册流程进行注册。

亚马逊为新用户提供了一项免费计划,旨在帮助你免费体验 AWS 服务。亚马逊为大多数服务提供免费的信用额度。随着时间的推移,该优惠可能会有所变动,因此本书不会详细介绍此优惠的具体内容,但详细信息可以在aws.amazon.com/free/找到。

完成注册流程后,你将进入 AWS 管理控制台的登录页面。由于 Amazon 现在有很多服务,这个页面可能让人有些不知所措,但你会很快习惯的。如果你喜欢使用书签,那么这个页面绝对是一个首选:

你刚刚创建的账户被称为 root 账户。此账户将始终拥有对所有资源的完全访问权限。因此,确保将你的密码保存在安全的地方。最佳做法是仅使用 root 账户通过 IAM 服务创建初始用户,我们将在稍后发现这个服务。此外,强烈建议切换到 多因素认证MFA)并使用身份服务 IAM 管理用户账户,因此请设置一个相对复杂的密码。

启用 root 账户的 MFA

为了避免任何问题,我们在完成注册后需要做的第一件事是启用 MFA。如果你以前没有听说过或看过 MFA,它是一种安全系统,要求通过来自不同类别凭证的多种身份验证方法。这些方法用于验证用户身份以便登录。实际上,一旦启用,你将需要在注册时设置的密码才能登录。此外,你还需要另一个来自不同来源的代码。第二个来源可以是通过物理设备(如 SafeNet IDProve,链接:amzn.to/2u4K1rR)提供,或者通过手机上的短信,或者通过安装在智能手机上的应用程序。我们将使用第三种选项——安装在智能手机上的应用程序,这完全免费:

  1. 前往你的应用商店、Google Play 商店或应用市场,安装一个名为 Google Authenticator 的应用(或者其他等效的应用,如 Authy)。

  2. 在 AWS 管理控制台中,打开右上角的“我的安全凭证”页面:

  1. 如果系统提示你创建并使用 AWS 身份与访问管理IAM)用户,点击“继续到安全凭证”按钮。我们将在第三章中探索 IAM 系统,将基础设施视为代码。在页面上展开“多因素认证”(MFA)部分。

  2. 选择虚拟 MFA 并按照说明将 Google 验证与 root 账户同步(注意,扫描二维码选项是最简单的配对设备方式)。

从现在开始,你将需要你的密码以及 MFA 应用中显示的令牌,才能作为 root 用户登录 AWS 控制台。

有两个关于管理密码和 MFA 的通用建议如下:

  • 有许多优秀的应用程序可以管理密码,例如1Password,可以在agilebits.com/onepassword找到,或者Dashlane,可以在www.dashlane.com找到。

  • 对于 MFA,你还可以尝试使用Authy,它可以在www.authy.com找到。它的工作原理类似于 Google Authenticator,但也有一个集中式服务器,允许它跨多个设备(包括桌面应用程序)工作,因此如果你丢失了手机,你也不会失去对 AWS 的访问权限。

正如我们之前所看到的,根账户的使用应该限制到最小。因此,为了创建虚拟服务器、配置服务等,我们将依赖 IAM 服务,这将让我们对每个用户的权限进行细粒度控制。

在 IAM 中创建新用户

在本节中,我们将为需要访问 AWS 的不同个人创建和配置账户。目前,我们将保持简单,仅为自己创建一个账户,如下所示:

  1. 在 AWS 控制台中导航到 IAM 菜单(console.aws.amazon.com/iam/),或者转到 AWS 控制台页面左上角的“服务”下拉菜单,搜索IAM

  1. 从导航窗格中选择“用户”选项。

  2. 通过点击“添加用户”按钮创建一个新用户,并确保勾选“编程访问”选项,以为该用户生成访问密钥 ID 和秘密访问密钥。

  3. 选择默认选项并创建用户。别忘了下载凭证。

  4. 返回到“用户”菜单,点击你的用户名以访问详细页面。

  5. 在“权限”标签下,点击“添加权限”按钮,选择“直接附加现有策略”选项。点击“AdministratorAccess”以向我们新创建的用户提供对 AWS 服务和资源的完全访问权限。

  6. 选中“AdministratorAccess”选项旁边的复选框,以向我们新创建的用户提供对 AWS 服务和资源的完全访问权限。你将看到如下的页面:

我们需要做的最后一件事是为此账户设置密码并启用 MFA。可以按照以下步骤完成:

  1. 点击“安全凭证”标签。

  2. 现在点击“控制台密码”选项,为新创建的用户启用密码。设置你选择的密码并点击“应用”按钮。

  3. 在你完成设置密码后,点击“分配 MFA 设备”选项。

  4. 选择“虚拟 MFA 设备”选项,并按照剩余的说明操作,以便在你新创建的账户中启用 MFA。你将看到一条信息,说明“MFA 设备已成功关联到你的账户”,如下所示:

  1. 此时,你已经准备好开始使用新创建的用户账户。需要注意的是,使用 IAM 用户账户登录与根账户不同,主要的区别是你使用不同的 URL 登录。

  2. 转到 console.aws.amazon.com/iam/home#home 或在 IAM 菜单中点击仪表盘。

  3. 你将在 IAM 用户登录链接下看到你的独特登录 URL。你也可以自定义该链接。将这个新 URL 保存到书签中,从现在开始,使用这个链接登录 AWS 控制台。

  4. 从根账户退出。

  5. 重新登录,但这次请使用你的 IAM 用户账户,地址是 https://AWS-account-IDalias.signin.aws.amazon.com/console

不要分享你的访问密钥和秘密密钥。通过这些步骤,我们强制要求使用 MFA 来访问 AWS 控制台。现在,我们需要两个因素(密码和 MFA 令牌)才能访问控制台。也就是说,我们还创建了一个访问密钥,它的安全性较低。任何拥有秘密密钥和访问密钥(这两者都包含在 credentials.csv 中)的人将拥有 AWS 账户的完全管理权限。请确保永远不要在网上共享这些凭证。在 第八章,加固你的 AWS 环境的安全性,我们将进行一些更改,以更好地保护这个密钥并要求使用 MFA 才能获得管理员权限。

配置账户的下一步是配置我们的计算机,以便通过命令行界面与 AWS 进行交互。

安装和配置命令行界面(CLI)

使用 Amazon 的网页界面通常是探索新服务的好方法。问题是,当你想要快速操作、创建更多可重复的步骤或生成良好的文档时,执行简单命令会更加高效。Amazon 提供了一个非常棒且易于使用的 CLI 工具。该工具是用 Python 编写的,因此具有跨平台特性(Windows、Mac 和 Linux)。

我们将在笔记本电脑/桌面电脑上安装该工具,以便通过 bash 命令与 AWS 交互。Linux 和 macOS X 原生支持 bash。如果你使用其中一种操作系统,可以跳过下一节。在 Windows 上,我们首先需要安装名为 Windows Subsystem for LinuxWSL)的功能,它将使我们能够运行与 Ubuntu Linux 上非常相似的 Bash 命令。

安装 WSL(仅限 Windows)

如今,Linux 和 macOS X 是开发人员使用的最主流操作系统之一。Windows 最近与 Canonical(Linux 发行版之一的背后公司)建立了合作关系,支持 Bash 以及大多数常见的 Linux 包。通过在 Windows 上安装此工具,我们将能够更高效地与我们的服务器交互,这些服务器也运行 Linux:

  1. 点击开始按钮,搜索 设置,然后打开设置应用:

  1. 这将引导您到以下窗口,在其中搜索 Windows 更新设置。打开 Windows 更新设置菜单:

  1. 在 Windows 更新设置的左侧菜单中,点击“开发人员”子菜单并启用“开发者模式”选项。

  2. 一旦开启开发者模式,在左侧菜单的搜索框中搜索 控制面板 选项:

  1. 在控制面板的仪表板中,选择“类别”视图选项,然后点击“程序”选项。接着,在“程序和功能”下,点击“启用或关闭 Windows 功能”选项:

  1. 在此菜单中,找到名为“Windows 子系统 for Linux (Beta)”的功能,并点击“确定”按钮:

这将安装该功能并要求您重新启动计算机。

  1. 返回 Windows 后,再次点击开始按钮,搜索 bash,并启动 Bash on Ubuntu on the Windows 应用:

  1. 完成初始化步骤后,您将能够像在 Linux 上一样使用 Windows 上的 Bash。

从此时起,使用 Bash 应用程序运行书中列出的命令。

安装 AWS CLI 包

AWS CLI 工具是用 Python 编写的。虽然有多种方式来安装它,但我们将使用 PyPA,Python 包管理器,来安装此工具。

要安装 PyPA,取决于您的操作系统,您需要运行以下命令:

  • 在 Windows 上:
$ sudo apt install python-pip
  • 在 macOS X 上:
$ sudo easy_install pip
  • 在基于 Debian 的 Linux 发行版上:
$ sudo apt-get install python-pip python-dev build-essential  
  • 在基于 Red Hat/CentOS 的 Linux 发行版上:
    $ sudo yum -y install python-pip  

安装 PyPA 后,您将能够使用 pip 命令。

最后,要使用 pip 命令安装 AWS CLI,您只需运行以下命令:

$ sudo pip install --upgrade --user awscli 

如果提示您升级 pip 版本到最新的可用版本,执行 pip install --upgrade pip

我们已经展示了来自 CentOS 基于 Linux 发行版的所有输出,但该过程同样适用于所有提到的支持平台。

配置 AWS CLI

为此,您需要从在创建 IAM 中的新用户部分的第 4 步下载的文件中提取 AWS 访问密钥 ID 和秘密访问密钥:

$ more credentials.csv
User Name,Access Key Id,Secret Access Key "yogeshraheja", AKIAII55DTLEV3X4ETAQ, mL2dEC8/ryuZ7fu6UI6kOm7PTlfROCZpai07Gy6T 

我们将运行以下命令来配置我们的 AWS 账户:

$ aws configure
AWS Access Key ID [None]: AKIAII55DTLEV3X4ETAQ
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]: 

此时,我们准备开始使用 CLI。我们可以通过列出用户账户来快速验证一切是否正常,如下所示:

$ aws iam list-users
{
    "Users": [
        {
            "UserName": "yogeshraheja",
            "PasswordLastUsed": "2018-08-07T09:57:53Z",
            "CreateDate": "2018-08-07T04:56:03Z",
            "UserId": "AIDAIN22VCQLK43UVWLMK",
            "Path": "/",
            "Arn": "arn:aws:iam::094507990803:user/yogeshraheja"
        }
    ]
}

AWS aws-shell

亚马逊有一个第二个命令行工具叫做aws-shell。这个工具比经典的awscli命令更具交互性,因为它提供了开箱即用的自动补全功能,并且有一个分屏视图,可以让你在输入命令时访问文档。如果你是一个新的 AWS 用户,不妨试试看(pip install aws-shell)。

创建我们的第一个 Web 服务器

现在我们已经设置好了环境,终于可以启动我们的第一个 EC2 实例了。有几种方法可以做到这一点。由于我们刚刚安装并配置了awscli,并且希望查看有效的基础设施管理方法,我们将演示如何使用 CLI 来完成这项工作。

启动虚拟服务器需要提前准备一些信息。我们将使用aws ec2 run-instances命令,但需要提供以下信息:

  • 一个 AMI ID

  • 一个实例类型

  • 一个安全组

  • 一个 SSH 密钥对

亚马逊机器镜像(AMI)

AMI 是一个包含多个内容的包,其中包括操作系统的根文件系统(例如,Linux、UNIX 或 Windows),以及启动系统所需的其他软件。为了找到合适的 AMI,我们将使用aws ec2 describe-images命令。默认情况下,describe-images命令会列出所有可用的公共 AMI,目前已经超过了 300 万个。为了充分利用这个命令,重要的是结合过滤选项,仅包括我们想要使用的 AMI。在我们的情况下,我们将使用以下方法来过滤我们的 AMI:

  • 我们希望名称为 Amazon Linux AMI,这表示 AWS 官方支持的 Linux 发行版。Amazon Linux 基于 Red Hat/CentOS,但包括一些额外的包,以便与其他 AWS 服务的集成更加方便。你可以在amzn.to/2uFT13F了解更多关于 AWS Linux 的信息。

  • 我们想使用x84_64位版本的 Linux,以匹配我们将要使用的架构。

  • 虚拟化类型应该是 HVM,代表硬件虚拟机。这是最新且性能最强的虚拟化类型。

  • 我们需要 GP2 支持,这样我们就可以使用最新一代的实例,而这些实例没有实例存储,意味着支持我们实例的服务器将与存储我们数据的服务器不同。

此外,我们将按年龄对输出进行排序,并只查看最近发布的 AMI:

$ aws ec2 describe-images --filters "Name=description,Values=Amazon Linux AMI * x86_64 HVM GP2" --query 'Images[*].[CreationDate, Description, ImageId]' --output text | sort -k 1 | tail

运行前面命令的输出如下所示:

如你所见,当前最新的 AMI ID 是ami-cfe4b2b0。当你执行相同的命令时,这可能会有所不同,因为亚马逊供应商定期更新他们的操作系统。

当使用aws cli --query选项时,输出对于某些命令非常重要。以前面的例子为例,如果我们只关心某些信息子集,我们可以使用--query选项来过滤我们想要的信息。该选项使用JMESPath查询语言。

实例类型

在本节中,我们将选择用于虚拟服务器的虚拟硬件。AWS 提供了一些选项,最好在他们的文档中进行详细描述:aws.amazon.com/ec2/instance-types/。我们将在第六章中更详细地讨论实例类型,扩展您的基础设施

现在,我们将选择t2.micro实例类型,因为它符合 AWS 的免费使用套餐条件。

安全组

安全组的工作方式有点像防火墙。所有 EC2 实例都有一组安全组分配给它们,每个安全组包含允许流入(入站)和/或流出(出站)流量的规则。

在这个练习中,我们将创建一个运行在tcp/3000端口上的小型 Web 应用程序。此外,我们希望能够通过 SSH 连接到实例,因此我们还需要允许到tcp/22端口的入站流量。我们将通过以下步骤创建一个简单的安全组来实现这一点:

  1. 首先,我们需要找出我们的默认虚拟私有云VPC)ID。尽管处于云环境中,物理资源由所有 AWS 客户共享,但安全性仍然受到严格强调。AWS 使用 VPC 的概念来分割他们的虚拟基础设施。你可以把它想象成一个带有自己网络的虚拟数据中心。保护我们的 EC2 实例的安全组与子网相关联,子网又与 VPC 提供的网络相关联:

要识别我们的 VPC ID,我们可以运行以下命令:

    $ aws ec2 describe-vpcs 
    {
        "Vpcs": [
            {
                "VpcId": "vpc-4cddce2a",
                "InstanceTenancy": "default",
                "CidrBlockAssociationSet": [
                    {
                        "AssociationId": "vpc-cidr-assoc-3c313154",
                        "CidrBlock": "172.31.0.0/16",
                        "CidrBlockState": {
                            "State": "associated"
                        }
                    }
                ],
                "State": "available",
                "DhcpOptionsId": "dopt-c0be5fa6",
                "CidrBlock": "172.31.0.0/16",
                "IsDefault": true
            }
        ]
    } 
  1. 现在我们知道了 VPC ID(你的将会不同),我们可以按照以下步骤创建我们的新安全组:
    $ aws ec2 create-security-group \
        --group-name HelloWorld \
        --description "Hello World Demo" \
        --vpc-id vpc-4cddce2a 
    {
        "GroupId": "sg-01864b4c"
    }
  1. 默认情况下,安全组允许实例的所有出站流量。我们只需打开 SSH(tcp/22)和tcp/3000的入站流量。然后,我们需要输入以下内容:
    $ aws ec2 authorize-security-group-ingress \
        --group-name HelloWorld \
        --protocol tcp \
        --port 22 \
        --cidr 0.0.0.0/0

    $ aws ec2 authorize-security-group-ingress \
        --group-name HelloWorld \
        --protocol tcp \
        --port 3000 \
        --cidr 0.0.0.0/0  
  1. 我们现在可以使用以下代码验证所做的更改,因为之前的命令并不详细:
    $ aws ec2 describe-security-groups \
        --group-names HelloWorld \
        --output text 
    SECURITYGROUPS  Hello World Demo    sg-01864b4c     HelloWorld      
    094507990803    vpc-4cddce2a
    IPPERMISSIONS   22      tcp     22
    IPRANGES        0.0.0.0/0
    IPPERMISSIONS   3000    tcp     3000
    IPRANGES        0.0.0.0/0
    IPPERMISSIONSEGRESS     -1
    IPRANGES        0.0.0.0/0 

如预期那样,我们已经开放了适当端口的流量。如果您知道如何找到您的公共 IP 地址,您可以通过将0.0.0.0/0替换为您的 IP/32 来改进 SSH 规则,以便只有您可以尝试通过 SSH 连接到该 EC2 实例。

使用 aws cli --output 选项

大多数命令默认会返回 JSON 输出。AWS 全局可用的选项有一定数量。您可以在本章中看到它们的使用。第一个选项是--output [json | text | table]

生成您的 SSH 密钥

默认情况下,Amazon EC2 使用 SSH 密钥对为你提供 SSH 访问权限。你可以在 EC2 中生成密钥对并下载私钥,也可以使用第三方工具(如 OpenSSL)生成密钥,并在 EC2 中导入公钥。我们将使用第一种方法来创建 EC2 SSH 密钥。

在此,确保为你新生成的私有(.pem)密钥文件设置只读权限:

    $ aws ec2 create-key-pair --key-name EffectiveDevOpsAWS --query   
    'KeyMaterial' --output text > ~/.ssh/EffectiveDevOpsAWS.pem

    $ aws ec2 describe-key-pairs --key-name EffectiveDevOpsAWS
    {
        "KeyPairs": [
            {
                "KeyName": "EffectiveDevOpsAWS",
                "KeyFingerprint": 
          "27:83:5d:9b:4c:88:f6:15:c7:39:df:23:4f:29:21:3b:3d:49:e6:af"

            }
        ]
    }

    $ cat ~/.ssh/EffectiveDevOpsAWS.pem
 -----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAiZLtUMnO2OKnHvTJOiIP26fThdsU0YRdlKI60in85x9aFZXSrZsKwOhWPpMtnUMJKeGvVQut+gJ1I1PNNjPqS2Dy60jH55hntUhr/ArpaL2ISDX4BgRAP1jcukBqS6+pL+mTp6OUNTToUt7LvAZoeo+10SYbzHF1ZMQLLs96fCMNvnbJdUCa904dJjJs7t/G2ou9RiNMRx8midrWcmmuGKOb1s6FgrxJ5OAMYegeccFVfGOjqPk3f+6QTPOTMNgNQ8ANKOMA9YtcIca/75QGUPifusTqUT4Fqtv3rbUYPvacAnYL9eCthtn1XMG7Oo/mR5MrU60wib2QcPipmrGNbwIDAQABAoIBABSyqkmxUxGGaCZcJbo9Ta16fnRxFZzAEWQ/VCIydv4+1UrSE7RS0zdavT8E3aP/Ze2LKtncu/wVSpJaFVHGVcWpfGKxvIG3iELZ9oUhDyTW/x3+IKanFRNyxyKudk+UyhuPRMu/7JhksV9mbbiILkfiPzSMSzpjB4p1hEkypfbvBnrbB+sRycx+jK5l209rNDukkJVvyFCnqPiH0wmvKRqHTNOMGWmM6CPOU+VpuMX+dIlrSeId7j6hqMjA0rGncnxYi035v2zicvIsEKHZ9MZCnkiRb3kJ9PhueTwwUQmoBYfV5E+1Wu34UmdsmALQEX3xniaR6xf9iWhQ2Nh8LaECgYEAzXHOZDPAUzXitO735KBUaiBp9NMv2gzE862Yf2rmDkFM4Y5RE3DKHrKfeOkrYqlG11On0m44GHBk/g4eqqIEaBjVp6i/Lk74tpQU6Kn1HT3w9lbXEFsCWjYZnev5oHP6PdedtRYNzZsCSNUdlw0kOG5WZZJ4E7mPZyrvK5pq+rMCgYEAq22KT0nD3d59V+LVVZfMzJuUBDeJeD139mmVbzAq9u5Hr4MkurmcIj8Q6jJIQaiC8XC1gBVEl08ZN2oY1+CBE+Gesi7mGOQ2ovDmoTfYRgScKKHv7WwR+N5/N7o26x+ZaoeaBe43Vjp6twaTpKkBOIuT50tvb25v9+UVMpGKcFUC
gYEAoOFjJ3KjREYpT1jnROEM2cKiVrdefJmNTel+RyF2IGmgg+1Hrjqf/OQSH8QwVmWK9SosfIwVX4X8gDqcZzDS1JXGEjIB7IipGYjiysP1D74myTF93u/16qD89H8LD0xjBTSo6lrn2j9tzY0eS+Bdodc9zvKhF4kzNC4Z9wJIjiMCgYAOtqstXP5zt5n4hh6bZxkL4rqUlhO1f0khnDRYQ8EcSp1agh4P7Mhq5BDWmRQ8lnMOuAbMBIdLmV1ntTKGrN1HUJEnaAEV19icqaKR6dIlSFYC4stODH2KZ8ZxiQkXqzGmxBbDNYwIWaKYvPbFJkBVkx1Rt9bLsKXpl/72xSkltQKBgQCYEjUVp4dPzZL1CFryOwV72PMMX3FjOflTgAWr8TJBq/OLujzgwYsTy6cdD3AqnMQ2BlU7Gk4mmDZCVVsMqHFbIHEa5Y4e5qIQhamedl3IgmnMpdyuDYaT/Uh4tw0JxIJabqm+sQZv4s1Otgh00JlGrgFs+0D39Fy8qszqr6J04w==
-----END RSA PRIVATE KEY----- 
    $ chmod 400 ~/.ssh/EffectiveDevOpsAWS.pem

启动 EC2 实例

我们现在已经拥有了启动实例所需的所有信息。让我们最后按如下方式启动它:

$ aws ec2 run-instances \
 --instance-type t2.micro \
 --key-name EffectiveDevOpsAWS \
 --security-group-ids sg-01864b4c \
 --image-id ami-cfe4b2b0
{
    "Instances": [
        {
            "Monitoring": {
                "State": "disabled"
            },
            "PublicDnsName": "",
            "StateReason": {
                "Message": "pending",
                "Code": "pending"
            },
            "State": {
                "Code": 0,
                "Name": "pending"
            },
            "EbsOptimized": false,
            "LaunchTime": "2018-08-08T06:38:43.000Z",
            "PrivateIpAddress": "172.31.22.52",
            "ProductCodes": [],
            "VpcId": "vpc-4cddce2a",
            "CpuOptions": {
                "CoreCount": 1,
                "ThreadsPerCore": 1
            },
            "StateTransitionReason": "",
            "InstanceId": "i-057e8deb1a4c3f35d",
            "ImageId": "ami-cfe4b2b0",
            "PrivateDnsName": "ip-172-31-22-52.ec2.internal",
            "KeyName": "EffectiveDevOpsAWS",
            "SecurityGroups": [
                {
                    "GroupName": "HelloWorld",
                    "GroupId": "sg-01864b4c"
                }
            ],
            "ClientToken": "",
            "SubnetId": "subnet-6fdd7927",
            "InstanceType": "t2.micro",
            "NetworkInterfaces": [
                {
                    "Status": "in-use",
                    "MacAddress": "0a:d0:b9:db:7b:38",
                    "SourceDestCheck": true,
                    "VpcId": "vpc-4cddce2a",
                    "Description": "",
                    "NetworkInterfaceId": "eni-001aaa6b5c7f92b9f",
                    "PrivateIpAddresses": [
                        {
                            "PrivateDnsName": "ip-172-31-22-
                             52.ec2.internal",
                            "Primary": true,
                            "PrivateIpAddress": "172.31.22.52"
                        }
                    ],
                    "PrivateDnsName": "ip-172-31-22-52.ec2.internal",
                    "Attachment": {
                        "Status": "attaching",
                        "DeviceIndex": 0,
                        "DeleteOnTermination": true,
                        "AttachmentId": "eni-attach-0428b549373b9f864",
                        "AttachTime": "2018-08-08T06:38:43.000Z"
                    },
                    "Groups": [
                        {
                            "GroupName": "HelloWorld",
                            "GroupId": "sg-01864b4c"
                        }
                    ],
                    "Ipv6Addresses": [],
                    "OwnerId": "094507990803",
                    "SubnetId": "subnet-6fdd7927",
                    "PrivateIpAddress": "172.31.22.52"
                }
            ],
            "SourceDestCheck": true,
            "Placement": {
                "Tenancy": "default",
                "GroupName": "",
                "AvailabilityZone": "us-east-1c"
            },
            "Hypervisor": "xen",
            "BlockDeviceMappings": [],
            "Architecture": "x86_64",
            "RootDeviceType": "ebs",
            "RootDeviceName": "/dev/xvda",
            "VirtualizationType": "hvm",
            "AmiLaunchIndex": 0
        }
    ],
    "ReservationId": "r-09a637b7a3be11d8b",
    "Groups": [],
    "OwnerId": "094507990803"
}

你可以跟踪实例创建的进度。为此,获取aws ec2 run-instances命令输出中的实例 ID,并运行以下命令:

$ aws ec2 describe-instance-status --instance-ids i-057e8deb1a4c3f35d
{
    "InstanceStatuses": [
        {
            "InstanceId": "i-057e8deb1a4c3f35d",
            "InstanceState": {
                "Code": 16,
                "Name": "running"
            },
            "AvailabilityZone": "us-east-1c",
            "SystemStatus": {
                "Status": "initializing",
                "Details": [
                    {
                        "Status": "initializing",
                        "Name": "reachability"
                    }
                ]
            },
            "InstanceStatus": {
                "Status": "initializing",
                "Details": [
                    {
                        "Status": "initializing",
                        "Name": "reachability"
                    }
                ]
            }
        }
    ]
}

一旦SystemStatus下的状态从initializing变为ok,实例就会准备好:

$ aws ec2 describe-instance-status --instance-ids i-057e8deb1a4c3f35d --output text| grep -i SystemStatus
SYSTEMSTATUS ok

使用 SSH 连接到 EC2 实例

本章的主要目标是创建一个简单的Hello World网页应用。由于我们从一个普通操作系统开始,我们需要连接到主机,进行必要的更改,将我们的标准服务器转变为网页服务器。为了通过 SSH 访问我们的实例,我们需要找到正在运行的实例的 DNS 名称,方法如下:

$ aws ec2 describe-instances \
 --instance-ids i-057e8deb1a4c3f35d \
 --query "Reservations[*].Instances[*].PublicDnsName"
[
    [
        "ec2-34-201-101-26.compute-1.amazonaws.com"
    ]
]      

我们现在有了实例的公共 DNS 名称和私钥,可以用来 SSH 连接到它。最后需要了解的是,对于我们在选择 Amazon Linux 的 AMI 时所选的操作系统,默认用户帐户名为ec2-user

$ ssh -i ~/.ssh/EffectiveDevOpsAWS.pem ec2-user@ ec2-34-201-101-26.compute-1.amazonaws.com

The authenticity of host 'ec2-34-201-101-26.compute-1.amazonaws.com (172.31.22.52)' can't be established.

ECDSA key fingerprint is SHA256:V4kdXmwb5ckyU3hw/E7wkWqbnzX5DQR5zwP1xJXezPU.

ECDSA key fingerprint is MD5:25:49:46:75:85:f1:9d:f5:c0:44:f2:31:cd:e7:55:9f.

Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'ec2-34-201-101-26.compute-1.amazonaws.com,172.31.22.52' (ECDSA) to the list of known hosts.

       __| __|_ )
       _| ( / Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2018.03-release-notes/

1 package(s) needed for security, out of 2 available

Run "sudo yum update" to apply all updates.
[ec2-user@ip-172-31-22-52 ~]$

如果遇到任何问题,请在 SSH 命令中添加-vvv选项进行故障排除。

创建一个简单的 Hello World 网页应用

现在我们已经连接到 EC2 实例,准备开始在它上面进行实验。在本书中,我们将专注于 AWS 在技术公司中最常见的用例:托管应用程序。在编程语言方面,我们将使用 JavaScript,这是 GitHub 上最受欢迎的语言之一。也就是说,这个应用程序更多的是为了提供支持,演示如何使用 DevOps 原则最好地使用 AWS。了解 JavaScript 的任何知识都不是理解本书的必要条件:

JavaScript 在本书中提供的一些主要优势包括:

  • 它非常容易写和读,甚至对于初学者来说

  • 它不需要编译

  • 它可以在服务器端运行,得益于 Node.js(nodejs.org

  • 它是 AWS 官方支持的,因此 AWS 的 JavaScript SDK 是一个一流的成员

在本章的其余部分,所有命令和代码都将在我们的实例上通过 SSH 运行。

安装 Node.js

我们需要做的第一件事是安装 Node.js。Amazon Linux 基于Red Hat Enterprise LinuxRHEL),并使用yum工具来管理和安装软件包。操作系统自带了Enterprise Linux 的额外软件包EPEL)预配置。正如我们所预期的,Node.js 在 EPEL 中:

[ec2-user@ip-172-31-22-52 ~]$ sudo yum install --enablerepo=epel -y nodejs 
[ec2-user@ip-172-31-22-52 ~]$ node -v
v0.10.48

这肯定是一个旧版本的节点,但它对我们需要的功能已经足够了。

运行 Node.js Hello World 应用程序

现在,节点已经安装好,我们可以创建一个简单的 Hello World 应用程序。下面是创建此应用程序的代码:

var http = require("http") http.createServer(function (request, response) {
// Send the HTTP header
// HTTP Status: 200 : OK
// Content Type: text/plain
response.writeHead(200, {'Content-Type': 'text/plain'})
// Send the response body as "Hello World" response.end('Hello World\n')
}).listen(3000)

// Console will print the message console.log('Server running')    

随意将其复制到文件中。或者,如果你想节省时间,可以从 GitHub 上下载:

[ec2-user@ip-172-31-22-52 ~]$ 
wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.js -O /home/ec2-user/helloworld.js
--2018-08-19 13:06:42-- https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.js
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.200.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.200.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 384 [text/plain]
Saving to: ‘/home/ec2-user/helloworld.js’

/home/ec2-user/helloworld.js 100%[=====================================================================================>] 384 --.-KB/s in 0s

2018-08-19 13:06:42 (37.9 MB/s) - ‘/home/ec2-user/helloworld.js’ saved [384/384] 

[ec2-user@ip-172-31-22-52 ~]$  

为了运行 Hello World 应用程序,我们现在只需运行以下代码:

[ec2-user@ip-172-31-22-52 ~]$ node helloworld.js  
Server running 

如果一切顺利,你现在应该能够在浏览器中打开以下链接:http://your-public-dns-name:3000。或者在我的例子中,可以在这里找到:http://ec2-34-201-101-26.compute-1.amazonaws.com:3000。然后,你将能够看到如下结果:

我们现在将通过在终端窗口中按 Ctrl + C 停止 Hello World 网络应用程序的执行。

使用 upstart 将我们的简单代码转化为服务

由于我们在终端手动启动了节点应用程序,关闭 SSH 连接或按 Ctrl + C 键会停止节点进程,因此我们的 Hello World 应用程序将不再工作。与标准的 Red Hat 基于发行版不同,Amazon Linux 带有一个名为 upstart 的系统。

这个工具非常易于使用,并提供了一些传统 System-V 启动 脚本没有的额外功能,比如在进程意外死亡时重新生成进程。要添加 upstart 配置,你需要在 EC2 实例的/etc/init目录下创建一个文件。

以下是将其插入/etc/init/helloworld.conf的代码:

description "Hello world Daemon"

# Start when the system is ready to do networking. Start on started elastic-network-interfaces

# Stop when the system is on its way down. Stop on shutdown

respawn script
exec su --session-command="/usr/bin/node /home/ec2-user/helloworld.js" ec2-user
end script

为什么要在弹性网络接口上启动? 如果你熟悉 AWS 之外的 upstart,可能曾使用过 start on run level [345]。在 AWS 中,问题在于你的网络来自 弹性网络接口 (ENI),如果应用程序在此服务之前启动,它可能无法正确连接到网络。

[ec2-user@ip-172-31-22-52 ~]$ 
sudo wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.conf -O /etc/init/helloworld.conf
--2018-08-19 13:09:39-- https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.conf
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.200.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.200.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 301 [text/plain]
Saving to: ‘/etc/init/helloworld.conf’

/etc/init/helloworld.conf 100%[=====================================================================================>] 301 --.-KB/s in 0s

2018-08-19 13:09:39 (54.0 MB/s) - ‘/etc/init/helloworld.conf’ saved [301/301]

[ec2-user@ip-172-31-22-52 ~]$

我们现在可以简单地启动我们的应用程序,如下所示:

[ec2-user@ip-172-31-22-52 ~]$ sudo start helloworld
helloworld start/running, process 2872
[ec2-user@ip-172-31-22-52 ~]$

正如预期的那样,http://your-public-dns-name:3000 仍然有效,这一次我们可以安全地关闭 SSH 连接。

终止我们的 EC2 实例

和大多数 Hello World 示例一样,一旦helloworld消息显示出来,目标就达成了。现在是时候考虑关闭我们的服务器了。由于我们在 AWS 中只为实际使用的资源付费,因此释放像这样的不必要的资源是一种使 AWS 非常具有成本效益的好策略。

我们可以使用stop命令干净地关闭 Hello World 服务。然后,我们可以退出虚拟服务器并终止实例,如下所示:

[ec2-user@ip-172-31-22-52 ~]$ sudo stop helloworld 
helloworld stop/waiting 
[ec2-user@ip-172-31-22-52 ~]$ ec2-metadata --instance-id 
instance-id: i-057e8deb1a4c3f35d 
[ec2-user@ip-172-31-22-52 ~]$ exit 
logout 
$ aws ec2 terminate-instances --instance-ids i-057e8deb1a4c3f35d 
{ 
    "TerminatingInstances": [ 
        { 
            "InstanceId": "i-057e8deb1a4c3f35d", 
            "CurrentState": { 
                "Code": 32, 
                "Name": "shutting-down" 
            }, 
            "PreviousState": { 
                "Code": 16, 
                "Name": "running" 
            } 
        } 
    ] 
} 

总结

本章是对 AWS 及其最著名的服务 EC2 的简短介绍。在注册 AWS 后,我们配置了环境,以便通过命令行界面创建虚拟服务器。接下来,我们选择了第一个 AMI,创建了第一个安全组,并生成了 SSH 密钥,这些密钥将在整本书中重复使用。在启动 EC2 实例后,我们手动部署了一个简单的 Node.js 应用程序来显示 Hello World。

虽然得益于 AWS CLI,该过程并不特别繁琐,但仍然需要经历许多步骤,而且这些步骤并不容易重复。我们还在没有任何自动化或验证的情况下部署了应用程序。此外,检查应用程序是否运行的唯一方法是手动检查端点。在接下来的章节中,我们将重新审视创建和管理网页应用程序与基础设施的过程,但这次我们将遵循 DevOps 原则,并结合其最佳实践。

在第三章《将基础设施视为代码》中,我们将讨论我们遇到的第一个问题:如何通过自动化来管理我们的基础设施。为此,我们将编写代码来管理我们的基础设施。

问题

请回答以下问题:

  1. 如何创建一个免费层的 AWS 账户?

  2. 如何使用 AWS 控制台门户创建你的第一个 AWS 云实例?

  3. 如何使用 AWS CLI 工具创建你的第一个 AWS 云实例?

  4. 如何在新创建的 AWS 实例上部署一个简单的 Hello World 网页应用程序?

  5. 如何销毁你创建的 AWS 实例以完成此练习?

进一步阅读

请参考以下链接,获取关于 AWS 和 AWS CLI 的更多信息:

第三章:将基础设施视为代码

在上一章中,我们已经熟悉了 AWS。我们还创建了一个 EC2 实例并将 Hello World 网页应用部署到该实例上。然而,为了完成这些操作,我们必须经历一系列步骤来配置实例及其安全组。由于我们使用命令行界面以非常手动的方式完成这些操作,这些步骤将不可复用,也不可审计,正如你可能还记得在第一章实现 DevOps 最佳实践时提到的那样。你应该尽可能依赖的两个关键概念是源代码管理(版本控制)和自动化。在本章中,我们将探讨如何将这些原则应用于我们的基础设施。

在云环境中,几乎所有内容都被抽象化并通过虚拟资源中介提供,我们可以很容易地想象,代码可以描述网络拓扑和系统配置。要完成这一转变,我们将学习 DevOps 组织中两个关键的概念。第一个通常被称为基础设施即代码IAC)。这是用代码的形式描述所有虚拟资源的过程。这些资源可能包括虚拟服务器、负载均衡器、存储、网络层等。第二个概念,虽然与 IAC 非常相近,但更专注于系统配置,被称为配置管理。通过配置管理系统,开发人员和系统管理员能够自动化操作系统配置、软件包安装,甚至应用程序部署。

进行这一转变是任何以 DevOps 为核心的组织的关键步骤。通过使用代码来描述不同的资源及其配置,我们将能够使用与开发应用程序时相同的工具和流程。我们将能够使用源代码管理,针对各个分支进行较小的更改,提交拉取请求,遵循标准的审核流程,并最终在将更改应用到生产环境之前进行测试。这将为我们提供更清晰的视角、更高的责任性以及更强的审计能力,以便管理基础设施的变化。正因如此,我们还能够管理更大规模的资源池,而不必增加更多的工程师,或者花费更多的时间来操作这些资源。这也为进一步的自动化打开了大门,正如我们在第五章中将看到的那样,添加持续集成和持续部署。在本章中,我们将覆盖以下主题:

  • 使用 CloudFormation 管理基础设施

  • 添加配置管理系统

技术要求

本章的技术要求如下:

  • AWS 控制台

  • AWS CloudFormation

  • AWS CloudFormation Designer

  • CloudFormer

  • Troposphere

  • Git

  • GitHub

  • Ansible

本章代码的 GitHub 链接如下:

使用 CloudFormation 管理基础设施

CloudFormation 引入了一种全新的方式来管理服务及其配置。通过创建 JSON 或 YAML 文件,CloudFormation 让你能够描述你希望构建的 AWS 架构。文件创建完成后,你只需将其上传到 CloudFormation,CloudFormation 将执行这些文件,自动创建或更新你的 AWS 资源。大多数 AWS 管理的工具和服务都支持这种方式。你可以在 amzn.to/1Odslix 查看到完整的支持列表。在本章中,我们将仅关注我们迄今为止所构建的基础设施,但在接下来的章节中我们会添加更多资源。在简要了解 CloudFormation 的结构之后,我们将创建一个最小化的堆栈,以重新创建来自第二章的 Hello World Web 应用程序,部署你的第一个 Web 应用程序。之后,我们将介绍另外两种创建 CloudFormation 模板的方式——设计器,它允许你在 Web GUI 中可视化编辑模板,以及 CloudFormer,这是一种可以从现有基础设施生成模板的工具。

开始使用 CloudFormation

正如你所期望的,你可以通过 AWS 控制台访问 CloudFormation,网址为 console.aws.amazon.com/cloudformation,或者使用以下命令行:

$ aws cloudformation help # for the list of options 

该服务围绕堆栈的概念进行组织。每个堆栈通常描述一组 AWS 资源及其配置,以便启动应用程序。在使用 CloudFormation 时,你大部分时间都会花费在编辑这些模板上。开始编辑模板有不同的方法,其中一种最简单的方法是编辑现有的模板。AWS 提供了多个编写良好的示例,网址为 amzn.to/27cHmrb。在最高层次上,模板的结构如下:

{ 
"AWSTemplateFormatVersion" : "version date", "Description" : "Description", "Resources" : { }, 
"Parameters" : { }, 
"Mappings" : { }, 
"Conditions" : { }, 
"Metadata" : { }, 
"Outputs" : { } 
} 

AWSTemplateFormatVersion 部分当前始终为 2010-09-09,表示使用的模板语言版本。这个版本目前是唯一有效的值。Description 部分用于总结模板的功能。Resources 部分描述了将实例化哪些 AWS 服务及其配置。当你启动模板时,你可以提供一些额外的信息给 CloudFormation,例如使用哪个 SSH 密钥对。例如,如果你想给 EC2 实例提供 SSH 访问权限,这类信息将放入 Parameters 部分。Mappings 部分在你尝试创建更通用的模板时非常有用。

例如,你可以定义在某个特定区域使用哪种 Amazon Machine Image (AMI),以便在该 AWS 区域启动应用程序时使用相同的模板。Conditions 部分允许你为其他部分添加条件逻辑(如 if 语句、逻辑运算符等),而 Metadata 部分则允许你为资源添加更多任意信息。最后,Outputs 部分让你根据模板的执行结果提取并打印出有用信息,例如创建的 EC2 服务器的 IP 地址。除了这些示例,AWS 还提供了一些与 CloudFormation 模板创建相关的工具和服务。你可以用来创建模板的第一个工具叫做 CloudFormation Designer。

AWS CloudFormation Designer

AWS CloudFormation Designer 是一个工具,允许你使用图形用户界面创建和编辑 CloudFormation 模板。Designer 隐藏了通过标准文本编辑器编辑 CloudFormation 模板时的大量复杂性。你可以直接访问 console.aws.amazon.com/cloudformation/designer,或者在点击创建堆栈按钮后通过 CloudFormation 仪表板访问,如下所示:

工作流程相当简单。你只需将资源从左侧菜单拖放到画布上。

一旦你的资源被添加,你就可以通过围绕每个资源图标的小圆点将它们连接到其他资源。在前面的示例中,我们将一个 EC2 实例连接到其安全组。有许多隐藏的宝藏可以帮助你在设计模板时。你可以右键点击资源,直接访问 CloudFormation 资源的文档,如下所示:

当拖动一个点来连接两个资源时,设计器将高亮显示与该连接兼容的资源。设计器底部的编辑器支持使用Ctrl + 空格键的自动补全功能:

一旦你的模板完成,你只需点击一个按钮,就可以从设计堆栈到启动堆栈。我们接下来要看的是名为CloudFormer的工具。

CloudFormer

CloudFormer 是一个通过查看现有资源来创建 CloudFormation 模板的工具。如果你已经创建了一些临时的资源集,正如我们在本书中所做的那样,那么你可以使用 CloudFormer 将它们分组到一个新的 CloudFormation 模板下。之后,你可以使用文本编辑器甚至 CloudFormation 设计器自定义 CloudFormer 生成的模板,使其符合你的需求。与大多数 AWS 工具和服务不同,CloudFormer 并非完全由 AWS 管理;它是一个自托管的工具,你可以通过 CloudFormation 按需实例化。为此,请按照以下步骤操作:

  1. 打开 https😕/console.aws.amazon.com/cloudformation 在你的浏览器中。

  2. 现在,向下滚动 AWS 控制台屏幕,选择“从现有资源创建模板”选项,然后点击“启动 CloudFormer”按钮。

  3. 在“选择一个样本模板”下拉菜单中,选择 CloudFormer 选项,然后点击“下一步”按钮,如下图所示:

  1. 在该屏幕上,顶部你可以提供堆栈名称(可以保留默认名称AWSCloudFormer),在下方,你将被要求提供三个额外的参数:用户名、密码和 VPC 选择。此用户名和密码将用于稍后登录 CloudFormer。选择一个用户名和密码,选择默认的 VPC,然后点击“下一步”按钮。

  2. 在下一屏幕上,你可以提供额外的标签和更高级的选项,但我们将继续点击“下一步”按钮。

  3. 这将带我们进入审查页面,在这里我们将勾选复选框以确认这将导致 AWS CloudFormation 创建 IAM 资源。点击 创建 按钮。

  4. 这将把我们带回 CloudFormation 控制台的主屏幕,在那里我们可以看到我们的 AWS CloudFormer 堆栈正在创建中。一旦状态列从 CREATE_IN_PROGRESS 变为 CREATE_COMPLETE,选择它并点击底部的 Outputs 标签。此时,你已经创建了使用 CloudFormer 所需的资源。为了使用它创建堆栈,请按照以下步骤操作:在 Outputs 标签(显示 CloudFormation 的 Outputs 部分)中,点击网站 URL 链接。这将打开 CloudFormer 工具。使用在前一组说明的第四步中提供的用户名和密码登录,你应该能看到类似以下内容:

  1. 选择你想要创建模板的 AWS 区域,然后点击 创建模板 按钮。接下来会出现以下屏幕:

  1. 按照工具所建议的工作流程选择你想要的不同资源,用于你的 CloudFormation 模板,直到最后一步。

  2. 最后,你将能够下载生成的模板或直接将其保存到 S3。

CloudFormer 生成的 CloudFormation 模板通常需要稍作编辑,因为你通常会希望创建一个更灵活的堆栈,带有输入参数和 Outputs 部分。

使用 CloudFormation 重新创建我们的 Hello World 示例

Designer 和 CloudFormer 是在构建基础设施并尝试为设计添加源代码控制时非常有用的两个工具。也就是说,每当你戴上 DevOps 帽子时,情况就不同了。使用这些工具显著减少了 CloudFormation 通过使用 JSON 格式提供的附加价值。如果你有机会阅读一些现有的模板,或者尝试在现有的基础设施上使用 CloudFormer,你可能会注意到原始的 CloudFormation 模板通常相当长,并且不符合 不要重复自己DRY)原则。

从 DevOps 的角度来看,CloudFormation 最强大的功能之一就是能够编写代码动态生成这些模板。为了说明这一点,我们将使用 Python 和一个名为 troposphere 的库来生成我们的 Hello World CloudFormation 模板。

还有一些更高级的工具可以帮助创建 CloudFormation 模板。如果你计划使用除 AWS 之外的其他第三方服务,可以看看 Hashicorp 提供的 Terraform(网址:www.terraform.io),它除了支持 CloudFormation 外,还处理其他多个云提供商和服务。

使用 Troposphere 创建我们模板的 Python 脚本

我们将首先安装troposphere库。同样,我们展示的是基于 CentOS 7.x 的 Linux 发行版的所有输出,但该过程同样适用于所有支持的平台。以下是安装troposphere库的命令:

$ pip install troposphere  

Troposphere 的一个已知问题是setuptools的升级版本。如果遇到以下问题,解决方案是使用pip install -U setuptools命令升级setuptools

一旦运行了上述命令,您可能会遇到以下错误:

....
setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)     
-----------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-pW4aV4/cfn-flip/        

为了解决这个错误,您可以运行以下命令:

$ pip install -U setuptools 
Collecting setuptools
 Downloading https://files.pythonhosted.org/packages/ff/f4/385715ccc461885f3cedf57a41ae3c12b5fec3f35cce4c8706b1a112a133/setuptools-40.0.0-py2.py3-none-any.whl (567kB)
 100% |████████████████████████████████| 573kB 22.2MB/s
Installing collected packages: setuptools
 Found existing installation: setuptools 0.9.8
 Uninstalling setuptools-0.9.8:
 Successfully uninstalled setuptools-0.9.8
Successfully installed setuptools-40.0.0 

安装完成后,您可以创建一个名为helloworld-cf-template.py的新文件。

我们将通过从troposphere模块导入多个定义来开始我们的文件,如下所示:

"""Generating CloudFormation template."""

from troposphere import (
    Base64,
    ec2,
    GetAtt,
    Join,
    Output,
    Parameter,
    Ref,
    Template,
)  

我们还将定义第一个变量,这将使后续的代码编辑变得更加容易。因为我们将通过在这个初始模板的基础上构建新脚本:

ApplicationPort = "3000"  

从代码的角度来看,我们将做的第一件事是初始化一个Template变量。在脚本结束时,模板将包含我们基础设施的完整描述,我们将能够简单地打印其输出以获取我们的 CloudFormation 模板:

t = Template() 

在本书中,我们将并行创建并运行多个 CloudFormation 模板。为了帮助我们识别给定堆栈中的内容,我们可以提供一个描述。在模板创建后,按如下方式添加描述:

add_description("Effective DevOps in AWS: HelloWorld web application") 

当我们使用 Web 命令行界面启动 EC2 实例时,我们选择了用于 SSH 访问主机的密钥对。为了不失去这一功能,我们的模板首先会包含一个参数,允许 CloudFormation 用户在启动 EC2 实例时选择使用哪个密钥对。为此,我们将创建一个Parameter对象,并通过提供标识符、描述、参数类型、参数类型的描述以及帮助做出正确决策的约束描述来初始化它。为了使这个参数出现在我们的最终模板中,我们还将使用模板类中定义的add_parameter()函数:

t.add_parameter(Parameter(
    "KeyPair",
    Description="Name of an existing EC2 KeyPair to SSH",
    Type="AWS::EC2::KeyPair::KeyName",
    ConstraintDescription="must be the name of an existing EC2 KeyPair.",
))

接下来我们将关注的是安全组。我们将按照之前为KeyPair参数所做的方式进行操作。我们希望将SSH/22TCP/3000开放给全球。端口3000之前在ApplicationPort变量中定义。此外,这次定义的信息不是像之前那样的参数,而是一个资源。因此,我们将使用add_resource()函数将该新资源添加如下:

t.add_resource(ec2.SecurityGroup(
    "SecurityGroup",
    GroupDescription="Allow SSH and TCP/{} access".format(ApplicationPort),
    SecurityGroupIngress=[
        ec2.SecurityGroupRule(
            IpProtocol="tcp",
            FromPort="22",
            ToPort="22",
            CidrIp="0.0.0.0/0",
        ),
        ec2.SecurityGroupRule(
            IpProtocol="tcp",
            FromPort=ApplicationPort,
            ToPort=ApplicationPort,
            CidrIp="0.0.0.0/0",
        ),
    ],
))

在下一部分中,我们将替换手动登录到我们的 EC2 实例并安装helloworld.js文件及其init脚本的需求。为此,我们将利用 EC2 提供的UserData功能。当您创建 EC2 实例时,UserData可选参数允许您提供一组命令,在虚拟机启动后执行(您可以在amzn.to/1VU5b3s上阅读更多有关此主题的内容)。UserData参数的一个限制是,脚本必须进行 base64 编码,才能添加到我们的 API 调用中。

我们将创建一个小脚本来重现我们在第二章中所做的步骤,部署您的第一个 Web 应用程序。在这里,我们将对我们的第一个 Web 应用程序的部署步骤进行 base-64 编码,并将其存储在一个名为ud的变量中。请注意,在ec2-userhome目录中安装应用程序并不非常整洁。现在,我们尽量保持与在第二章中所做的步骤一致,部署您的第一个 Web 应用程序。我们将在第五章,添加持续集成和持续部署中解决这个问题,并改进我们的部署系统。

ud = Base64(Join('\n', [
    "#!/bin/bash",
    "sudo yum install --enablerepo=epel -y nodejs",
    "wget http://bit.ly/2vESNuc -O /home/ec2-user/helloworld.js",
    "wget http://bit.ly/2vVvT18 -O /etc/init/helloworld.conf",
    "start helloworld"
]))

现在我们将关注我们模板的主要资源,即我们的 EC2 实例。创建该实例需要提供一个用于标识资源的名称、一个镜像 ID、一个实例类型、安全组、用于 SSH 访问的密钥对,以及用户数据。为了简化操作,我们将硬编码 AMI ID(ami-cfe4b2b0)和实例类型(t2.micro)。

创建 EC2 实例所需的其余信息是安全组信息和KeyPair名称,这些信息我们之前通过定义参数和资源收集过。在 CloudFormation 中,您可以通过使用Ref关键字引用模板中预先存在的子部分。在 Troposphere 中,这可以通过调用Ref()函数来完成。如前所述,我们将借助add_resource函数将生成的输出添加到我们的模板中。

...
t.add_resource(ec2.Instance(
    "instance",
    ImageId="ami-cfe4b2b0",
    InstanceType="t2.micro",
    SecurityGroups=[Ref("SecurityGroup")],
    KeyName=Ref("KeyPair"),
    UserData=ud,
)) 
...

在脚本的最后部分,我们将专注于生成模板的Outputs部分,该部分将在 CloudFormation 创建堆栈时填充。此选择允许您打印在堆栈启动过程中计算出的有用信息。在我们的例子中,有两条有用的信息——访问我们 Web 应用程序的 URL 和实例的公网 IP 地址,以便我们如果需要,可以通过 SSH 访问它。为了获取这些信息,CloudFormation 使用Fn::GetAtt函数。在 Troposphere 中,这被转换为GetAtt()函数。

...
t.add_output(Output(
    "InstancePublicIp",
    Description="Public IP of our instance.",
    Value=GetAtt("instance", "PublicIp"),
))

t.add_output(Output(
    "WebUrl",
    Description="Application endpoint",
    Value=Join("", [
        "http://", GetAtt("instance", "PublicDnsName"),
        ":", ApplicationPort
    ]),
)) 
...

到那时,我们可以让我们的脚本输出我们生成的模板的最终结果。

print t.to_json() 

脚本现在已经完成。我们可以保存并退出编辑器。创建的文件应该与以下链接中的文件类似:raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter03/EffectiveDevOpsTemplates/helloworld-cf-template-part-1.py

现在我们可以运行脚本,给予适当的权限,并通过将脚本的输出保存到文件中来生成 CloudFormation 模板,如下所示:

$ python helloworld-cf-template.py > helloworld-cf.template 

cloud-init 是一组与大多数 Linux 发行版和云提供商兼容的 Python 脚本。它通过将大多数标准操作(如安装包、创建文件和运行命令)移动到模板的不同部分,来补充 UserData 字段。本书不涉及该工具,但如果你的 CloudFormation 模板严重依赖 UserData 字段,请查看它。你可以在bit.ly/1W6s96M获取文档。

在 CloudFormation 控制台中创建堆栈

到此为止,我们可以按照以下步骤启动我们的模板:

  1. 在浏览器中打开 CloudFormation 网络控制台,使用以下链接:console.aws.amazon.com/cloudformation。点击“创建堆栈”按钮。

  2. 在下一个页面中,我们将通过选择“上传模板到 Amazon S3”来上传我们新生成的模板helloworld-cf.template,然后浏览并选择我们的helloworld-cf.template文件。

  3. 然后我们将选择一个堆栈名称,例如HelloWorld

  4. 在堆栈名称之后,我们可以看到模板的参数部分正在发挥作用。CloudFormation 允许我们选择使用哪个 SSH 密钥对。使用下拉菜单选择你的密钥对。

  5. 在下一个页面中,我们可以为我们的资源添加可选标签;在“高级”部分,我们可以看到如何将 CloudFormation 与 SNS 集成,决定在发生故障或超时时采取什么行动,甚至添加堆栈策略来控制谁可以编辑堆栈。例如,暂时我们只需点击“下一步”按钮。

  6. 这将带我们到审查页面,我们可以在其中验证所选的信息,甚至估算运行该堆栈的费用。点击“创建”按钮。

  7. 这将带我们进入主要的 CloudFormation 控制台。在该页面上,我们可以在“事件”标签中查看资源的创建情况。

  8. 当模板创建完成后,点击“输出”标签页,这将显示我们通过模板的输出部分生成的信息,如下所示:

  1. 点击 WebUrl 键的值中的链接,将打开我们的 Hello World 页面。

将我们的模板添加到源控制系统中

既然我们已经测试了模板并确认它正常工作,我们将把它提交到我们的源代码管理系统。这将帮助我们跟踪更改,使我们能够像管理应用代码一样,管理基础设施代码(更多内容将在第五章 添加持续集成与持续部署 中讲解)。

为此,我们将依赖 Git。AWS 提供了一个名为 AWS CodeCommit 的服务(amzn.to/2tKUj0n),可以让你轻松管理 Git 仓库。然而,由于该服务的流行程度远低于 GitHub(github.com),我们将使用后者。如果你还没有 GitHub 账号,先注册一个——完全免费。

登录 GitHub 后,为 CloudFormation 模板创建一个新的仓库:

  1. 在浏览器中打开 github.com/new

  2. 将新的仓库命名为:EffectiveDevOpsTemplates

  3. 勾选“Initialize this repository with a README”选项框。

  4. 最后,点击此处显示的“Create repository”按钮:

  1. 创建仓库后,你需要将其克隆到你的电脑中。为此,你需要安装 Git(如果尚未安装,可以通过 Google 查找安装 Git 的操作系统相关教程)。对于 CentOS,只需运行 yum -y install git,因为 Git 包现在是 Linux 发行版的一部分:
$ git clone https://github.com/<your_github_username>/EffectiveDevOpsTemplates 
  1. 既然仓库已经被克隆下来,我们将进入该仓库并将之前创建的模板复制到新的 GitHub 仓库中:
$ cd EffectiveDevOpsTemplates
$ cp <path_to_helloworld_template>/helloworld-cf-template.py .
  1. 最后,我们将把新文件添加并提交到项目中,并将其推送到 GitHub,如下所示:
$ git add helloworld-cf-template.py
$ git commit -m "Adding helloworld Troposphere template"
$ git push  

单一代码仓库与多仓库:在管理代码时,有两种常见的方法来组织代码仓库。你可以为每个项目创建一个仓库,或者决定将整个组织的代码放在一个仓库中。我们将在本书中选择最简单的选项——每个项目一个仓库,但随着像谷歌的 Bazel、Facebook 的 Buck 或 Twitter 的 Pants 等多个开源项目的发布,使用单一仓库(monorepo)变得非常具有吸引力,因为它可以避免在对基础设施和服务进行大规模更改时,在多个仓库之间来回切换。

更新我们的 CloudFormation 堆栈

使用 CloudFormation 模板管理资源的最大优势之一是,CloudFormation 创建的资源与我们的堆栈紧密绑定。如果我们想对堆栈进行更改,可以更新模板,并将更改应用到现有的 CloudFormation 堆栈中。让我们来看看这个过程是如何工作的。

更新我们的 Python 脚本

我们的helloworld-cf-template.py脚本非常基础。目前,我们仅仅在使用 Python 的troposphere库来以比手动编写更愉快的方式生成 JSON 输出。当然,你可能已经意识到,我们仅仅触及了编写脚本来创建和管理基础设施所能做的事情的冰山一角。接下来的部分是一个简单的示例,将让我们编写几行 Python 代码,演示如何更新 CloudFormation 堆栈,并利用更多的服务和外部资源。

我们在前面的示例中创建的安全组向世界开放了两个端口:22(SSH)和3000(Web 应用端口)。我们可以通过只允许自己的 IP 使用 SSH 来加固安全性。这意味着需要在处理端口22流量的安全组中更改 Python 脚本中的无类域间路由CIDR)IP 信息。网上有许多免费的服务可以帮助我们查看自己的公网 IP。我们将使用其中一个服务,地址为api.ipify.org。我们可以通过一个简单的curl命令查看其效果:

$ curl https://api.ipify.org 54.164.95.231  

我们将在脚本中利用这个服务。使用这个特定服务的原因之一是它已经被打包成了一个 Python 库。你可以在github.com/rdegges/python-ipify上了解更多内容。你可以首先按如下方式安装该库:

$ pip install ipify

如果遇到一些与pip相关的错误,如以下代码块所示,可以通过降级pip版本,安装ipify,然后再次将pip升级到最新版本来修复:

Cannot uninstall 'requests'. It is a distutils installed project and thus we cannot accurately determine which files belong to it which would lead to only a partial uninstall.    

前面的错误可以通过以下命令修复:

$ pip install --upgrade --force-reinstall pip==9.0.3
$ pip install ipify
$ pip install --upgrade pip  

我们的脚本需要一个 CIDR。为了将 IP 地址转换为 CIDR,我们还将安装另一个名为ipaddress的库。结合这些库的主要优势是,我们不必担心处理 IPv4 与 IPv6 的问题:

$ pip install ipaddress 

一旦这些库安装完成,重新打开helloworld-cf-template.py文件。在脚本的顶部,我们将导入这些库,然后,在ApplicationPort变量定义后,我们将定义一个名为PublicCidrIp的新变量,并结合前面提到的两个库,我们可以按如下方式提取 CIDR:

...
from ipaddress import ip_network
from ipify import get_ip
from troposphere import (
    Base64,
    ec2,
    GetAtt,
    Join,
    Output,
    Parameter,
    Ref,
    Template,
)

ApplicationPort = "3000"
PublicCidrIp = str(ip_network(get_ip()))
...

最后,我们可以按如下方式更改 SSH 组规则的CidrIp声明:

SecurityGroupIngress=[
        ec2.SecurityGroupRule(
            IpProtocol="tcp",
            FromPort="22",
            ToPort="22",
            CidrIp=PublicCidrIp,
        ),    
....
    ]

我们现在可以保存这些更改。创建的文件应该与github.com/yogeshraheja/Effective-DevOps-with-AWS/blob/master/Chapter03/EffectiveDevOpsTemplates/helloworld-cf-template.py中的文件类似。

我们现在可以生成一个新的diff命令来直观地验证更改:

$ python helloworld-cf-template.py > helloworld-cf-v2.template
$ diff helloworld-cf-v2.template helloworld-cf.template 
46c46
<             "CidrIp": "54.164.95.231/32",
---
>             "CidrIp": "0.0.0.0/0",
 91a92
>
$

如我们所见,我们的 CIDR IP 现在已正确限制了对我们 IP 的连接。我们现在可以应用该更改。

更新我们的堆栈

生成新的 JSON CloudFormation 模板后,我们可以进入 CloudFormation 控制台并按照以下步骤更新堆栈:

  1. 在浏览器中打开 CloudFormation Web 控制台,访问console.aws.amazon.com/cloudformation

  2. 选择我们之前创建的 HelloWorld 堆栈。

  3. 点击“操作”下拉菜单,然后选择“更新堆栈”选项。

  4. 通过点击“浏览”按钮,选择 helloworld-cf-v2.template 文件,然后点击“下一步”按钮。

  5. 这将带我们到下一个屏幕,让我们更新堆栈的详细信息。在我们的例子中,参数没有发生变化,因此我们可以继续点击“下一步”按钮。

  6. 在下一个屏幕中,由于我们只希望看到 IP 更改的效果,我们可以点击“下一步”按钮:

  1. 这将带我们到审核页面,在几秒钟后,我们可以看到 CloudFormation 给我们预览更改:

  1. 如你所见,唯一的变化将是对安全组的更新。现在点击“更新”按钮。这将带我们回到 CloudFormation 模板,在那里我们将看到更改被应用。

  2. 在这个特定的示例中,AWS 能够简单地更新安全组,以便考虑到我们的更改。我们可以通过提取物理 ID 来验证更改,无论是在审核页面,还是回到控制台中的“资源”标签页:

 $ aws ec2 describe-security-groups \
 --group-names HelloWorld-SecurityGroup-1XTG3J074MXX

更改集

我们的模板只包括一个 Web 服务器和一个安全组,这使得更新 CloudFormation 成为一个相对无害的操作。此外,我们的更改也相对简单,因为 AWS 只需更新现有的安全组,而无需替换它。正如你所想象的那样,随着架构变得越来越复杂,CloudFormation 模板也会变得更复杂。根据你想要执行的更新,你可能会在更新模板的最终步骤中审查更改集时遇到意想不到的变化。AWS 提供了一种替代且更安全的更新模板的方式;这个功能被称为更改集,可以通过 CloudFormation 控制台访问。请按照以下步骤使用更改集来审查更新,并执行操作:

  1. 在浏览器中打开 CloudFormation Web 控制台,访问console.aws.amazon.com/cloudformation

  2. 选择我们之前创建的 HelloWorld 堆栈

  3. 点击“操作”下拉菜单,然后点击“为当前堆栈创建更改集”选项

从这里,你可以按照 更新我们的堆栈 部分中进行简单更新时所采取的相同步骤操作。主要的区别出现在最后一个屏幕,如下所示:

与常规堆栈更新不同,变更集强调在应用变更之前让你有机会审核变更。如果你对显示的变更感到满意,你就可以执行更新。最后,在使用变更集更新堆栈时,你可以通过 CloudFormation 控制台中的“变更集”标签轻松审核最近的变更。最后,我们将使用以下命令将变更提交到 Troposphere 脚本:

$ git commit -am "Only allow ssh from our local IP"
$ git push 

删除我们的 CloudFormation 堆栈

在上一节中,我们看到 CloudFormation 如何在更新模板时更新资源。同样的操作适用于当你想要删除 CloudFormation 堆栈及其资源时。只需点击几下,你就可以删除模板以及启动时创建的各种资源。从最佳实践的角度来看,强烈建议始终使用 CloudFormation 来对以前通过 CloudFormation 初始化的资源进行更改,包括在你不再需要堆栈时。

删除堆栈非常简单,你应该按以下步骤进行:

  1. 在浏览器中打开 CloudFormation Web 控制台:console.aws.amazon.com/cloudformation

  2. 选择我们之前创建的HelloWorld堆栈

  3. 点击“操作”下拉菜单,然后点击“删除堆栈”选项

和往常一样,你可以在“事件”标签中跟踪完成情况:

CloudFormation 在 AWS 生态系统中占有独特地位。尽管架构复杂,绝大多数架构可以通过 CloudFormation 描述和管理,从而让你对 AWS 资源的创建保持严格控制。虽然 CloudFormation 在资源创建管理上做得非常好,但并不总是让事情变得简单。特别是当你想要在像 EC2 这样的服务上进行简单更改时,情况尤其如此。因为 CloudFormation 不会跟踪资源启动后的状态,所以更新 EC2 实例的唯一可靠方法是,例如,重新创建一个新的实例,并在其准备好后将其与现有实例进行交换。这种做法创造了某种不可变设计(假设在实例创建时没有运行任何额外的命令)。这可能是一个有吸引力的架构选择,并且在某些情况下,它可能会带你走得很远,但你也可能希望能够拥有长期运行的实例,这样你就可以像我们在 CloudFormation 中所做的那样,通过受控的管道快速而可靠地进行更改。这正是配置管理系统的强项。

添加一个配置管理系统

配置管理系统可能是经典 DevOps 驱动型组织中最为人熟知的组成部分。它们存在于大多数公司(包括企业市场中),并迅速替代了自制的 Shell、Python 和 Perl 脚本。有很多理由说明配置管理系统应该成为你环境的一部分。一个原因是,它们提供了领域特定语言,这提升了代码的可读性,而且它们是专门为组织在配置系统时出现的特定需求量身定制的。这也导致了很多有用的内置功能。此外,最常见的配置管理工具都有庞大且活跃的用户社区,这通常意味着你能找到已有的代码来自动化你正在使用的系统。

一些最流行的配置管理工具包括PuppetChefSaltStackAnsible。虽然这些选项都相当不错,本书将重点介绍 Ansible,它是四个工具中最易用的。Ansible 之所以成为一种非常流行且易于使用的解决方案,有几个关键特性。与其他配置管理系统不同,Ansible 被设计为无需服务器、守护进程或数据库即可运行。你可以将代码保存在版本控制中,并在需要时通过 SSH 使用推送机制将其下载到主机上。你编写的自动化代码是 YAML 静态文件,这使得学习曲线比使用 Ruby 或特定 DSL 的其他替代方案平缓得多。为了存储我们的配置文件,我们将依赖版本控制系统(在我们的案例中是 GitHub)。

AWS OpsWorks 及其 Chef 集成:虽然亚马逊并没有真正发布一个专门用于配置管理的服务,但它在 OpsWorks 服务中支持 Chef 和 Puppet。与我们在本书中探索的其他服务不同,OpsWorks 旨在成为一个完整的应用程序生命周期管理,包括资源配置、配置管理应用程序部署、软件更新、监控和访问控制**。如果你愿意牺牲一些灵活性和控制,OpsWorks 可能能够满足你运行简单 Web 应用程序的需求。你可以在amzn.to/1O8dTsn了解更多信息。

开始使用 Ansible

首先在你的电脑上安装 Ansible。安装完成后,创建一个 EC2 实例,我们将通过它来演示 Ansible 的基本用法。接下来,我们将重新创建 Hello World Node.js 应用程序,通过创建和执行 Ansible 所称之为的 playbook。然后,我们将探讨 Ansible 如何在拉取模式下运行,这为部署更改提供了一种新的方法。最后,我们将研究如何将UserData块替换为 Ansible,以便结合 CloudFormation 和我们的配置管理系统的优势。

Ansible 非常易于使用,并且在网络上有丰富的文档资源。本书将涵盖足够的内容,帮助你入门并快速掌握简单的配置,例如我们示例中需要的配置。然而,你可能会有兴趣花更多时间学习 Ansible,以便能更高效地使用它。

在你的计算机上安装 Ansible

如前所述,Ansible 是一个非常简单的应用程序,依赖项非常少。你可以通过操作系统的包管理器或使用pip来安装 Ansible,因为 Ansible 是用 Python 编写的。我们将演示所有的输出来自基于 CentOS 7.x 的 Linux 发行版,但该过程同样适用于所有受支持的平台。(更多信息,请参阅以下链接,了解如何在操作系统上查找并安装 Ansible 二进制文件:docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#installing-the-control-machine。)以下命令将安装一些二进制文件、库和 Ansible 模块:

$ yum install ansible 

请注意,在此时没有安装守护进程或数据库。这是因为默认情况下,Ansible 依赖于静态文件和 SSH 来运行。此时,我们已经准备好使用 Ansible 了:

$ ansible --version

ansible 2.6.2
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules',  
  u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-
  packages/ansible
  executable location = /bin/ansible
  python version = 2.7.5 (default, Aug 4 2017, 00:39:18) [GCC 4.8.5 
  20150623 (Red Hat 4.8.5-16)] 

创建我们的 Ansible 沙箱环境

为了展示 Ansible 的基本功能,我们将从重新启动我们的 Hello World 应用程序开始。

在上一节中,我们展示了如何使用 Web 界面创建堆栈。正如你所期望的,也可以通过命令行界面启动堆栈。进入你之前生成helloworld-cf-v2.template文件的EffectiveDevOpsTemplates目录,并运行以下命令:

$ aws cloudformation create-stack \
 --capabilities CAPABILITY_IAM \
 --stack-name ansible \
 --template-body file://helloworld-cf-v2.template \
 --parameters ParameterKey=KeyPair,ParameterValue=EffectiveDevOpsAWS
{
 "StackId": "arn:aws:cloudformation:us-east-
     1:094507990803:stack/ansible/bb29cb10-9bbe-11e8-9ee4-500c20fefad2"
}  

我们的实例很快就会准备好。现在我们可以通过创建一个工作区来引导我们的环境。

创建我们的 Ansible 仓库

使用 Ansible,我们的第一个目标是能够在远程主机上运行命令。为了高效地完成这项任务,我们需要配置本地环境。因为我们不想重复进行这些步骤,并且最终我们希望将所有内容都进行版本控制,所以我们将创建一个新的 Git 仓库。为此,我们将重复之前在创建EffectiveDevOpsTemplate仓库时所使用的相同步骤。

登录到 GitHub 后,为 CloudFormation 模板创建一个新的仓库,如下所示:

  1. 在浏览器中,打开这个链接:github.com/new

  2. 给新的仓库命名为ansible,如图所示:

  1. 勾选“初始化该仓库并附加 README”复选框。

  2. 最后,点击“创建仓库”按钮。

  3. 一旦仓库创建完成,将其克隆到你的计算机上,如下所示:

$ git clone https://github.com/<your_github_username>/ansible
  1. 现在仓库已被克隆,我们将进入该仓库并将之前在新的 GitHub 仓库中创建的模板复制进去:
$ cd ansible

从根本上讲,Ansible 是一个可以在你清单中的主机上远程执行命令的工具。清单可以通过手动创建一个INI文件来管理,文件中列出所有的主机和/或 IP 地址。如果能够查询 API,也可以动态管理清单。正如你可以想象的那样,Ansible 完全可以利用 AWS API 来获取我们的清单。为此,我们将从官方 Ansible Git 仓库下载一个 Python 脚本,并授予执行权限,命令如下:

**$ curl -Lo ec2.py** **http://bit.ly/2v4SwE5** **$ chmod +x ec2.py**  

在我们开始测试这个 Python 脚本之前,我们还需要为它提供一个配置文件。在相同目录下创建一个新文件,并命名为ec2.ini。在这个文件中,我们将放入以下配置:

[ec2] 
regions = all 
regions_exclude = us-gov-west-1,cn-north-1 destination_variable = public_dns_name vpc_destination_variable = ip_address route53 = False 
cache_path = ~/.ansible/tmp cache_max_age = 300 
rds = False 

完成这些步骤后,你可以通过执行如下命令验证清单是否正常工作:

$ ./ec2.py  

该命令应返回一个包含不同资源的大型嵌套 JSON,这些资源是你 AWS 账户中找到的。在这些资源中,有我们在上一节中创建的 EC2 实例的公共 IP 地址。我们引导过程的最后一步是配置 Ansible 本身,使其知道如何获取我们基础设施的清单;当它尝试通过 SSH 连接到我们的实例时,使用哪个用户;如何获取 root 权限;等等。我们将在相同位置创建一个新文件,并命名为ansible.cfg。其内容应如下所示:

[defaults] 
inventory      = ./ec2.py 
remote_user  = ec2-user 
become = True 
become_method  = sudo 
become_user    = root 
nocows     = 1 

到此为止,我们已经准备好开始运行 Ansible 命令。Ansible 有一些命令和简单的概念。我们将首先了解 ansible 命令和模块的概念。

执行模块

ansible 命令是驱动不同模块在远程主机上执行的主要命令。模块是可以直接在远程主机上执行的库。Ansible 附带了许多模块,模块列表可以在 bit.ly/24rU0yk 中找到。除了标准模块外,你还可以使用 Python 创建自己的自定义模块。这些是最常见用例和技术的模块。我们将首先看到的模块是一个简单的模块,叫做 ping,它尝试连接到主机并返回 pong,如果主机可用的话。

模块文档也可以通过使用 ansible-doc 命令访问,命令如下:

$ ansible-doc <Module-Name>

$ ansible-doc ping

这里,ping 是 Ansible 模块之一的名称。

在创建 Ansible 游乐场部分时,我们使用 CloudFormation 创建了一个新的 EC2 实例。到目前为止,我们还没有查找该实例的 IP 地址。使用 Ansible 和 ping 模块,我们将发现该信息。如前所述,我们需要处于 ansible 目录下才能运行 ansible 命令。命令如下:

$ ansible --private-key ~/.ssh/EffectiveDevOpsAWS.pem ec2 -m ping 
18.206.223.199 | SUCCESS => {
    "changed": false,
    "ping": "pong"
} 

正如我们所见,Ansible 通过查询 AWS EC2 API 成功找到了我们的 EC2 实例。新创建的实例现在已经准备好可以使用了。

配置 SSH:由于 Ansible 强烈依赖 SSH,因此值得花点时间通过 $HOME/.ssh/config 文件配置 SSH。例如,你可以使用以下选项来避免在前面的示例中指定 --private-key-u

IdentityFile ~/.ssh/EffectiveDevOpsAWS.pem

User ec2-user StrictHostKeyChecking no

PasswordAuthentication no

ForwardAgent yes

配置完成后,你将不再需要为 Ansible 提供 --private-key 选项。

运行任意命令

ansible 命令也可以用于在远程服务器上运行任意命令。在以下示例中,我们将仅在匹配 18.206.223.* 公共 IP 地址的主机上运行 df 命令(你需要根据前面 ping 命令中返回的实例公共 IP 调整此命令):

$ ansible --private-key ~/.ssh/EffectiveDevOpsAWS.pem '18.206.223.*' \
-a 'df -h'
18.206.223.199 | SUCCESS | rc=0 >>
Filesystem  Size  Used  Avail  Use%  Mounted on
devtmpfs    484M  56K   484M   1%    /dev
tmpfs       494M   0    494M   0%    /dev/shm
/dev/xvda1  7.8G   1.1G 6.6G   15%   /

现在我们对 Ansible 的工作原理有了基本的了解,我们可以开始结合调用不同的 Ansible 模块来实现自动化。这就叫做创建 playbook

Ansible playbooks

Playbooks 是包含 Ansible 配置、部署和编排语言的文件。通过创建这些文件,你可以按顺序定义系统的状态,从操作系统配置到应用程序部署和监控。Ansible 使用 YAML,易于阅读。因此,类似于我们在 CloudFormation 中所做的,开始使用 Ansible 的一个简单方法是查看官方 Ansible GitHub 仓库中的一些示例,网址是 github.com/ansible/ansible-examples。或者,你甚至可以查看我的仓库,该仓库使理解 playbooks 变得相对简单,并且可以在 github.com/yogeshraheja/Automation-with-Ansible-By-Yogesh-Raheja 找到,这本书是 Automation with Ansible

创建一个 playbook

Ansible 在其网站上提供了一些最佳实践,网址是 bit.ly/1ZqdcLH。他们文档中的一个重点是使用角色。组织 playbook 内容的一个关键方法是 Ansible 的 角色 组织功能,这部分内容在主 playbooks 页面中有详细说明。创建角色是使 Ansible 代码可共享和模块化的关键,这样你就可以在不同的服务和 playbooks 中重复使用你的代码。为了演示正确的结构,我们将创建一个角色,然后在我们的 playbook 中调用它。

创建角色以部署和启动我们的 web 应用程序

我们将使用角色重新创建之前使用 CloudFormation 的 UserData 块创建的 Hello World 堆栈。如果你还记得,UserData 部分大致是这样的:

yum install --enablerepo=epel -y nodejs 
wget http://bit.ly/2vESNuc -O /home/ec2-user/helloworld.js 
wget http://bit.ly/2vVvT18 -O /etc/init/helloworld.conf start helloworld

你会注意到在前面的脚本中有三种不同类型的操作。首先,我们准备系统来运行我们的应用程序。为了做到这一点,在我们的示例中,我们只是安装了一个 Node.js 包。接下来,我们复制运行应用所需的不同资源。在我们的例子中,这些资源是 JavaScript 代码和启动配置。最后,我们启动服务。像编程中的其他任务一样,保持代码的 DRY(Don't Repeat Yourself)原则非常重要。如果部署和启动我们的应用程序非常独特于 Hello World 项目,安装 Node.js 可能不是。为了使 Node.js 的安装成为可重用的代码,我们将创建两个角色——一个用于安装 Node.js,另一个用于部署和启动 Hello World 应用程序。

默认情况下,Ansible 期望在 Ansible 仓库的根目录下看到一个 roles 目录。因此,首先我们需要进入在 创建我们的 Ansible 仓库 部分下创建的 ansible 目录。在其中创建 roles 目录,并通过以下命令进入它:

$ mkdir roles
$ cd roles  

现在我们可以创建我们的角色了。Ansible 提供了一个 ansible-galaxy 命令,可以用来初始化角色的创建。我们将首先查看安装 Node.js 的角色:

$ ansible-galaxy init nodejs
- nodejs was created successfully  

如前所述,Ansible 和大多数其他配置管理系统一样,拥有一个强大的支持社区,社区成员通过 https😕/galaxy.ansible.com/ 分享角色。在使用 ansible-galaxy 命令创建新角色的骨架时,你还可以使用 ansible-galaxy 导入和安装社区支持的角色。

这会创建一个 nodejs 目录,并生成若干子目录,帮助我们结构化角色的不同部分。我们可以使用以下命令进入该目录:

$ cd nodejs  

nodejs 目录中最重要的目录是名为 tasks 的目录。当 Ansible 执行一个 playbook 时,它会运行 tasks/main.yml 文件中的代码。用你喜欢的文本编辑器打开这个文件。

当你第一次打开 tasks/main.yml 文件时,你将看到以下内容:

--- # tasks file for nodejs  

nodejs 角色的目标是安装 Node.js 和 npm。为此,我们将以类似于我们在 UserData 脚本中所做的方式进行操作,使用 yum 命令来执行这些任务。

在 Ansible 中编写任务时,你会按顺序调用多个 Ansible 模块。我们首先要查看的模块是 yum 命令的包装器。有关它的文档可以在 bit.ly/28joDLe 上找到。它将允许我们安装我们的包。我们还将引入循环的概念。由于我们有两个包需要安装,我们将希望调用 yum 模块两次。我们将使用操作符的 with_items 所有的 Ansible 代码都是用 YAML 编写的,这非常容易入门并使用。在初始的三个破折号和注释之后(这表示 YAML 文件的开始),我们将调用 yum 模块来安装我们的包:

--- 
# tasks file for nodejs 

name: Installing node and npm yum: 
name: "{{ item }}" enablerepo: epel state: installed 
with_items: 
nodejs 
npm 

每当 Ansible 运行该剧本时,它将检查系统上已安装的包。如果没有找到 nodejsnpm 包,它将会安装它们。

你的文件应该看起来像在 github.com/yogeshraheja/Effective-DevOps-with-AWS/blob/master/Chapter03/ansible/roles/nodejs/tasks/main.yml 中提供的示例一样。第一个角色完成了。为了本书的目的,我们保持这个角色非常简单,但你可以想象,在更具生产性质的环境中,你可能会有一个角色来安装特定版本的 Node.js 和 npm,直接从 nodejs.org/en/ 获取二进制文件,甚至可能安装特定的依赖项。我们的下一个角色将专注于部署并启动我们之前构建的 Hello World 应用程序。我们将返回到 roles 目录,向上移动一层,并再次调用 ansible-galaxy

$ cd ..
$ ansible-galaxy init helloworld
- helloworld was created successfully  

如同之前一样,我们现在将进入新创建的 helloworld 目录,方法如下:

$ cd helloworld  

这一次,我们将探索一些其他的目录。我们在运行 ansible-galaxy 命令时创建的子目录之一就是名为 files 的目录。将文件添加到该目录可以让我们在远程主机上复制文件。为了实现这一点,我们将首先按照以下步骤下载这两个文件:

$ wget http://bit.ly/2vESNuc -O files/helloworld.js
$ wget http://bit.ly/2vVvT18 -O files/helloworld.conf  

我们现在可以使用任务文件在远程系统上执行复制操作。打开 tasks/main.yml 文件,在最初的三个破折号和注释之后,添加以下内容:

--- 
# tasks file for helloworld 
- name: Copying the application file copy: 
src: helloworld.js dest: /home/ec2-user/ owner: ec2-user group: ec2-user 
mode: 0644 
notify: restart helloworld 

我们正在利用文档中提到的 copy 模块,参考链接为 http😕/bit.ly/1WBv08E,将我们的应用程序文件复制到 ec2-user 的主目录。在该调用的最后一行,我们在末尾添加了一个 notify 选项(注意 notify 语句如何与 copy 模块的调用对齐)。通知动作是可以添加到每个任务块末尾的触发器。在这个例子中,我们告诉 Ansible,当文件 helloworld.js 发生变化时调用重启 helloworld 指令,而如果代码没有发生变化,则不执行重启操作(稍后我们会在另一个文件中定义如何重启 helloworld 应用程序)。

CloudFormation 和 Ansible 之间的一个主要区别是,Ansible 预计将在系统生命周期内多次运行。Ansible 内置的许多功能都是针对长时间运行的实例进行了优化。因此,notify 选项使得在系统状态发生变化时触发事件变得非常容易。类似地,当遇到错误时,Ansible 会自动停止执行,以尽量避免出现故障。

现在我们已经复制了应用程序文件,可以添加我们的第二个文件——upstart 脚本。在前一个 helloword.js 文件的复制调用之后,我们将添加以下调用:

- name: Copying the upstart file copy: 
src: helloworld.conf 
dest: /etc/init/helloworld.conf owner: root 
group: root mode: 0644 

我们需要执行的最后一个任务是启动我们的服务。我们将使用 service 模块来实现。模块文档可以在 bit.ly/22I7QNH 找到:

- name: Starting the HelloWorld node service service: 
name: helloworld state: started 

我们的任务文件现在已经完成。你最终应该得到类似于 github.com/yogeshraheja/Effective-DevOps-with-AWS/blob/master/Chapter03/ansible/roles/helloworld/tasks/main.yml 的文件。

完成任务文件后,我们将继续处理下一个文件,该文件将让 Ansible 知道何时以及如何重启 helloworld,正如我们在任务的 notify 参数中所指定的那样。这些类型的交互在角色的 handler 部分定义。我们将编辑 handlers/main.yml 文件。在这里,我们也将使用 service 模块。以下是一个注释:

---
# handlers file for helloworld

将以下内容添加到 main.yml 文件:

- name: restart helloworld service: 
name: helloworld state: restarted 

这里没有什么意外;我们使用的是之前用于管理服务的相同模块。我们在角色中需要一个额外的步骤。为了使 helloworld 角色正常工作,系统需要安装 Node.js。Ansible 支持角色依赖的概念。我们可以明确告知我们的 helloworld 角色依赖于我们之前创建的 nodejs 角色,因此,如果执行 helloworld 角色,它将首先调用 nodejs 角色并安装运行应用所需的依赖。

打开 meta/main.yml 文件。该文件分为两个部分。第一部分在 galaxy_info 下,允许您填写您正在构建的角色信息。如果您愿意,最终可以将您的角色发布到 GitHub 并将其链接回 ansible-galaxy,与 Ansible 社区共享您的创作。文件底部的第二部分称为 dependencies,这是我们想要编辑的部分,确保在启动应用之前系统上存在 nodejs。去掉方括号([]),并添加以下条目来调用 nodejs

dependencies: 
- nodejs 

您的文件应与 github.com/yogeshraheja/Effective-DevOps-with-AWS/blob/master/Chapter03/ansible/roles/helloworld/meta/main.yml 中的示例类似。这标志着角色代码的创建完成。从文档编写的角度来看,良好的做法是编辑 README.md。完成后,我们可以继续创建一个将引用我们新创建角色的 playbook 文件。

创建 playbook 文件

在我们 Ansible 仓库的顶层(位于 helloworld 角色的上两层目录),我们将创建一个名为 helloworld.yml 的新文件。在此文件中,我们将添加以下内容:

--- 
- hosts: "{{ target | default('localhost') }}" become: yes 
roles: 
- helloworld 

这基本上告诉 Ansible 将 helloworld 角色应用到 target 变量中列出的主机上,或者如果未定义目标,则应用于 localhostbecome 选项将告诉 Ansible 以提升的权限执行该角色(在我们的例子中是 sudo)。此时,您的 Ansible 仓库应与 github.com/yogeshraheja/Effective-DevOps-with-AWS/tree/master/Chapter03/ansible 中的示例类似。我们现在准备好测试我们的 playbook 了。

请注意,在实际操作中,规模更大的角色部分可能不止一个角色。如果您向目标部署多个应用或服务,您经常会看到像这样的 playbook。在后续章节中,我们将看到更多的示例:

--- 
hosts: webservers roles: 
foo 
bar 
baz 

执行 playbook

执行 playbooks 使用专门的 ansible-playbook 命令。此命令依赖于我们之前使用的相同的 Ansible 配置文件,因此,我们希望从 Ansible 仓库的根目录运行此命令。命令的语法如下:

ansible-playbook <playbook.yml> [options] 

我们将首先运行以下命令(调整private-key选项的值):

$ ansible-playbook helloworld.yml \
    --private-key ~/.ssh/EffectiveDevOpsAWS.pem \
    -e target=ec2 \
    --list-hosts

-e(或--extra-vars)选项允许我们传递额外的执行选项。在我们的案例中,我们将target变量(在剧本的hosts部分声明)定义为ec2。这个第一个ansible-playbook命令将告诉 Ansible 以 EC2 实例为目标。--list-hosts选项会让 Ansible 返回一个符合主机标准的主机列表,但不会实际对这些主机执行任何操作。命令的输出结果类似于以下内容:

playbook: helloworld.yml 
  play #1 (ec2): ec2 TAGS:[] 
    pattern: [u'ec2'] 
    hosts (1): 
      18.206.223.199 

list-hosts选项是验证清单的好方法,对于更复杂的剧本,尤其是有更具体主机值的情况,它能够帮助验证哪些主机会运行实际的剧本,从而确保它们指向你期望的主机。

我们现在知道,如果我们使用这个值作为目标,将会影响哪些主机。接下来我们要检查的是,如果我们运行我们的剧本,会发生什么。ansible-playbook命令有一个-C(或--check)选项,它会尝试预测给定剧本将会进行的更改;有时这也被称为 Ansible 中的干运行模式:

$ ansible-playbook helloworld.yml \
 --private-key ~/.ssh/EffectiveDevOpsAWS.pem \
 -e target=18.206.223.199 \
 --check

PLAY [18.206.223.199] **************************************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************************************
ok: [18.206.223.199]

TASK [nodejs : Installing node and npm] ********************************************************************************************************************************
changed: [18.206.223.199] => (item=[u'nodejs', u'npm'])

TASK [helloworld : Copying the application file] ***********************************************************************************************************************
changed: [18.206.223.199]

TASK [helloworld : Copying the upstart file] ***************************************************************************************************************************
changed: [18.206.223.199]

TASK [helloworld : Starting the HelloWorld node service] ***************************************************************************************************************
changed: [18.206.223.199]

RUNNING HANDLER [helloworld : restart helloworld] **********************************************************************************************************************
changed: [18.206.223.199]

PLAY RECAP *************************************************************************************************************************************************************
18.206.223.199 : ok=6 changed=5 unreachable=0 failed=0

运行该命令会在干运行模式下执行我们的剧本。通过这种模式,我们可以确保正确的任务会被执行。由于我们处于干运行模式,一些模块可能无法找到它们需要的一切资源来模拟它们的执行方式。这就是为什么我们有时会看到服务启动错误出现在服务模块末尾。如果你看到这种情况,不用担心,等到在实时模式中安装软件包时,任务会正确执行。经过验证主机和代码后,我们最终可以运行ansible-playbook并在实时模式下执行我们的更改,如下所示:

$ ansible-playbook helloworld.yml \
    --private-key ~/.ssh/EffectiveDevOpsAWS.pem \
    -e target=18.206.223.199 

输出结果与--check命令非常相似,唯一不同的是这次执行是在实时模式下进行的。我们的应用程序现在已经安装并配置完毕,接下来我们可以通过以下方式验证其是否正常运行:

$ curl 18.206.223.199:3000
Hello World  

我们成功地使用 Ansible 重现了之前使用 CloudFormation 所做的操作。现在我们已经测试了第一个剧本,我们可以提交我们的更改。我们将分两次提交,以便拆分仓库的初始化和角色的创建。在你的 Ansible 仓库的根目录下,运行以下命令:

$ git add ansible.cfg ec2.ini ec2.py
$ git commit -m "Configuring ansible to work with EC2"
$ git add roles helloworld.yml
$ git commit -m "Adding role for nodejs and helloworld"
$ git push  

金丝雀测试变更

使用 Ansible 管理服务的一个巨大好处是,你可以轻松地对代码进行更改并快速推送该更改。在一些情况下,如果你有一大批由 Ansible 管理的服务,你可能希望仅将更改推送到单个主机,以确保一切如你所预期的那样运行。这通常被称为金丝雀测试。使用 Ansible,执行这一操作非常简单。为了说明这一点,我们将打开roles/helloworld/files/helloworld.js文件,然后简单地将第 11 行的响应从Hello World更改为Hello World, Welcome again

// Send the response body as "Hello World" 
response.end('Hello World, Welcome again\n'); 
}).listen(3000); 

保存文件,然后再次运行ansible-playbook。首先使用--check选项执行:

$ ansible-playbook helloworld.yml \
    --private-key ~/.ssh/EffectiveDevOpsAWS.pem \
    -e target=18.206.223.199 \
    --check

这次,Ansible 只检测到两个更改。第一个覆盖了应用程序文件,第二个执行了notify语句,这意味着重新启动应用程序。看到这正是我们预期的,我们可以在没有--check选项的情况下运行我们的 playbook:

$ ansible-playbook helloworld.yml \
    --private-key ~/.ssh/EffectiveDevOpsAWS.pem \
    -e target=18.206.223.199  

这产生的输出与我们之前的命令相同,但这次更改已经生效:

$ curl 18.206.223.199:3000
Hello World, Welcome again  

我们的更改非常简单,但如果我们通过更新 CloudFormation 模板来完成,CloudFormation 就需要创建一个新的 EC2 实例来实现这一点。在这里,我们只是更新了应用程序的代码,并通过 Ansible 将其推送到目标主机。现在,我们将在 Git 中本地撤销这一更改,操作如下:

$ git checkout roles/helloworld/files/helloworld.js  

我们将通过移除 EC2 实例上的更改来演示这一点,同时说明一个新的概念。在下一节中,我们将以异步方式在反向模式下(在这种情况下为拉模式)运行 Ansible。

越早越好:能够在几秒钟内推送更改而不是几分钟,可能看起来是一个小的胜利,但其实不是。速度很重要,它是区分成功的初创公司和技术的关键因素。能够在几分钟内而不是几天内部署新服务器是云计算采纳的一个重要因素。同样,容器的近期成功,正如我们在本书后面看到的,可能也是因为运行一个新容器只需要几秒钟,而启动一个虚拟服务器仍然需要几分钟。

以拉模式运行 Ansible

能够像刚才那样即时进行更改是一个非常有价值的功能。我们可以轻松并同步地推送新的代码,并验证 Ansible 执行是否成功。在更大规模上,虽然能够在一批服务器上做任何更改仍然像我们的示例一样有价值,但有时也会更加复杂。以这种方式进行更改的风险是,你必须非常有纪律地避免只将更改推送到一部分主机,而忽略其他也在共享刚刚更新角色的主机。否则,随着 Ansible 配置库和运行中的服务器之间的更改越来越多,运行 Ansible 就会变得更加危险。对于这些情况,通常最好使用一种拉取机制,它会自动拉取更改。当然,你不必选择其中一个——很容易配置推送和拉取机制来部署更改。Ansible 提供了一条名为ansible-pull的命令,顾名思义,它使得在拉模式下运行 Ansible 变得更加容易。ansible-pull命令的工作方式与ansible-playbook非常相似,不同之处在于它是通过从 GitHub 仓库中拉取代码开始的。

在我们的 EC2 实例上安装 Git 和 Ansible

由于我们需要能够远程运行 Ansible 和 Git,因此我们首先需要在 EC2 实例上安装这些软件包。现在,我们将通过手动安装这两个软件包来完成这项工作。稍后在本章中,我们会实现一个可重用的解决方案。由于 Ansible 是一个非常适合执行远程命令的工具,并且它有一个模块可以管理大多数常见需求,如安装软件包,因此,我们将不通过 ssh 登录到主机并运行一些命令,而是使用 Ansible 来推送这些更改。我们将从 EPEL yum 仓库安装 Git 和 Ansible。这将需要以 root 身份运行命令,你可以借助 become 选项来实现。调整 EC2 实例的 IP 地址后,运行以下命令:

$ ansible '18.206.223.199' \
    --private-key ~/.ssh/EffectiveDevOpsAWS.pem \
    --become \
    -m yum -a 'name=git enablerepo=epel state=installed' 
$ ansible '18.206.223.199' \
    --private-key ~/.ssh/EffectiveDevOpsAWS.pem \
    --become \
    -m yum -a 'name=ansible enablerepo=epel state=installed'

使用 ansible-pull,我们的目标是让 Ansible 在本地应用更改。我们可以对 Ansible 仓库进行修改,以优化这一操作。

配置 Ansible 在 localhost 上运行

由于ansible-pull依赖于 Git 来本地克隆仓库并执行操作,我们不需要通过 SSH 来进行执行。请进入你的 Ansible 仓库的root目录,创建一个新文件。文件名应为localhost,并且文件内容应包含以下内容:

[localhost] 
localhost ansible_connection=local 

本质上,我们所做的就是创建一个静态清单,并要求 ansible 在目标主机为 localhost 时以本地模式(而不是使用 SSH)运行命令。我们可以保存更改并将新文件提交到 GitHub,如下所示:

$ git add localhost
$ git commit -m "Adding localhost inventory"
$ git push 

向我们的 EC2 实例添加一个 cron 作业

现在我们将创建一个 cron 表条目,以定期调用 ansible-pull。在这里,我们同样依赖于 Ansible 来远程创建我们的 cron 作业。运行以下命令时,请根据需要调整 IP 地址:

$ ansible '18.206.223.199' \
--private-key ~/.ssh/EffectiveDevOpsAWS.pem \
-m cron -a 'name=ansible-pull minute="*/10" job="/usr/bin/ansible-pull -U https://github.com/<your_username>/ansible helloworld.yml -i localhost --sleep 60"'  

在前面的命令中,我们告诉 Ansible 使用 cron 模块,目标是我们的 ec2 实例。在这里,我们提供了一个名称,Ansible 将用它来跟踪 cron 作业,告诉 cron10 分钟运行一次该作业,接着是要执行的命令及其参数。我们给 ansible-pull 的参数包括我们分支的 GitHub URL,我们刚刚添加到仓库中的清单文件,以及一个 sleep 参数,这将使命令在启动后的 160 秒之间某个时刻启动。这有助于分散网络负载,防止如果我们有多个服务器时,所有节点服务同时重启。等待片刻后,我们可以通过以下命令验证更改是否生效:

$ curl 54.175.86.38:3000
Hello World  

在通过 CloudFormation 手动将 Ansible 集成到我们创建的 EC2 实例后,现在可以正式化该过程。

将 Ansible 与 CloudFormation 集成

尽管有不同的策略可以将 Ansible 集成到 CloudFormation 中,但在我们的情况下,显然有一条明确的路径可以选择。我们将利用 UserData 字段,并通过 ansible-pull 命令初始化 Ansible。

现在我们将启动本章之前创建的 Troposphere 脚本。我们将复制它,并将新脚本命名如下:

ansiblebase-cf-template.py. 

转到你的模板仓库,并按如下方式复制先前的模板:

$ cd EffectiveDevOpsTemplates 
$ cp helloworld-cf-template.py ansiblebase-cf-template.py 

接下来,使用你的编辑器打开ansiblebase-cf-template.py脚本。为了保持脚本的可读性,我们将首先定义几个变量。在应用端口声明之前,我们将定义一个应用名称:

ApplicationName = "helloworld" 
ApplicationPort = "3000" 

我们还将设置一些关于 GitHub 信息的常量。将GithubAccount的值替换为你的 GitHub 用户名或 GitHub 组织名称,如下所示:

ApplicationPort = "3000" 

GithubAccount = "EffectiveDevOpsWithAWS" 
GithubAnsibleURL = "https://github.com/{}/ansible".format(GithubAccount) 

在定义了GithubAnsibleURL之后,我们将创建一个新变量,该变量包含我们希望执行的命令行,用于通过 Ansible 配置主机。我们将调用ansible-pull并使用我们刚刚定义的GithubAnsibleURLApplicationName变量。它看起来像这样:

AnsiblePullCmd = \ 
"/usr/bin/ansible-pull -U {} {}.yml -i localhost".format( GithubAnsibleURL, 
ApplicationName 
) 

现在我们将更新UserData块。我们将不再安装 Node.js、下载应用文件并启动服务,而是将这个块更改为安装gitansible,执行AnsiblePullCmd变量中包含的命令,最后创建一个 cron 任务,每10分钟重新执行该命令。删除之前的ud变量定义,并用以下内容替换:

ud = Base64(Join('\n', [ "#!/bin/bash", 
"yum install --enablerepo=epel -y git", "pip install ansible", 
AnsiblePullCmd, 
"echo '*/10 * * * * {}' > /etc/cron.d/ansible- pull".format(AnsiblePullCmd) 
])) 

现在我们可以保存文件,使用它来创建我们的 JSON 模板,并进行测试。你新的脚本应该像这个示例一样:github.com/yogeshraheja/EffectiveDevOpsTemplates/blob/master/ansiblebase-cf-template.py:

$ python ansiblebase-cf-template.py > ansiblebase.template
$ aws cloudformation update-stack \
 --stack-name ansible \
 --template-body file://ansiblebase.template \
 --parameters ParameterKey=KeyPair,ParameterValue=EffectiveDevOpsAWS
{
"StackId": "arn:aws:cloudformation:us-
east-1:511912822958:stack/HelloWorld/ef2c3250-6428-11e7-a67b-50d501eed2b3"
}

你甚至可以自己创建一个新的堆栈。例如,假设是helloworld,而不是更改现有的ansible堆栈。在这种情况下,你需要运行以下命令来创建堆栈:

$ aws cloudformation create-stack \
 --stack-name helloworld \
 --template-body file://ansiblebase.template \
 --parameters ParameterKey=KeyPair,ParameterValue=EffectiveDevOpsAWS
{
 "StackId": "arn:aws:cloudformation:us-east-
     1:094507990803:stack/helloworld/5959e7c0-9c6e-11e8-b47f-
     50d5cd26c2d2"
} 

现在我们可以等待执行完成:

$ aws cloudformation wait stack-update-complete \
        --stack-name ansible

现在堆栈创建完成,我们可以查询 CloudFormation 获取堆栈的输出,特别是它的公共 IP 地址:

$ aws cloudformation describe-stacks \
    --stack-name ansible \
    --query 'Stacks[0].Outputs[0]'
  {
     "Description": "Public IP of our instance.",
     "OutputKey": "InstancePublicIp",
     "OutputValue": "35.174.138.51"
  }  

最后,我们可以验证服务器是否已启动并正常运行,如下所示:

$ curl 35.174.138.51:3000
Hello World  

现在我们可以将新创建的troposphere脚本提交到我们的 GitHub 仓库,如下所示:

EffectiveDevOpsTemplates repository:
$ git add ansiblebase-cf-template.py
$ git commit -m "Adding a Troposphere script to create a stack that relies on Ansible to manage our application"
$ git push  

我们现在拥有了一个完整的解决方案,可以通过代码高效地管理我们的基础设施。我们通过一个非常简单的示例演示了这一点。不过,正如你所想象的,所有这些也适用于更大规模的基础设施和更多的服务。本节几乎结束;我们现在可以删除我们的堆栈,以释放我们当前消耗的资源。在本章前面部分,我们通过 Web 界面做到了这一点。正如你所想象的,这也可以通过以下命令行接口轻松完成:

$ aws cloudformation delete-stack --stack-name ansible  

请注意,如果你为这个示例创建了新的helloworld堆栈,则可以使用以下命令将其删除:

aws cloudformation delete-stack --stack-name helloworld 

监控

如你所知,监控和度量一切是 DevOps 驱动组织的一个重要方面。在互联网上,你会找到许多撰写良好的博客文章和示例,展示如何高效地监控 CloudFormation 和 Ansible。在处理 CloudFormation 的监控时,你需要订阅与你的堆栈创建相关的 SNS 主题,以接收所有与堆栈生命周期相关的事件。关注 CloudFormation 堆栈创建失败也非常重要。Ansible 有一个回调系统,它同样提供了围绕 Ansible 执行创建自动化的方式。与 CloudFormation 类似,当 Ansible 执行失败时接收通知也很重要(当 Ansible 配置为拉取模式时,这一点尤为重要)。

摘要

在本章中,我们学习了如何通过代码高效地管理基础设施。我们还探索了 CloudFormation,这是一个 AWS 服务,允许你为不同的服务创建模板,以描述每个 AWS 组件及其配置。为了简化这些模板的创建,我们看了几个选项,从带有图形用户界面的 CloudFormation 设计工具,到 Python 库 Troposphere。随后,我们研究了配置管理,这是 DevOps 思想中最著名的方面之一。为了说明这个话题,我们研究了 Ansible,这是最流行的配置管理解决方案之一。我们首先查看了使用 Ansible 命令的不同方式,并在我们的基础设施上运行了简单的命令。然后我们学习了如何创建 playbook,这使我们能够编排不同的步骤来部署我们的 Web 服务器。最后,我们讨论了如何在拉取模式下使用 Ansible,这通常在管理大规模基础设施时更为合理。

我们现在有一个良好的生产环境,已准备好托管任何应用程序,我们也看到了如何设计它并监控我们的服务器。在第五章,添加持续集成与持续部署,我们将继续使用 CloudFormation 和 Ansible,但将在软件交付的背景下进行:我们将学习如何实施持续集成测试和持续部署。

问题

  1. IaC 代表什么?

  2. 如何使用 AWS CloudFormation 控制台部署一个简单的 Hello World 应用程序?

  3. 列出一些流行的 SCM 提供商。GitHub 账户在源代码管理中有何作用?

  4. 安装 Git(本地版本控制)包,克隆你在之前示例中创建的 GitHub 全局仓库,并将你的 helloworld-cf.template 推送到 GitHub 仓库。

  5. 什么是 Ansible?列出它的一些重要特性。

深入阅读

为了更详细地探索这个话题,请访问以下链接:

第四章:使用 Terraform 实现基础设施即代码

在第三章《将基础设施视为代码》中,我们熟悉了 AWS CloudFormation 和 Ansible。我们创建了一个 CloudFormation 模板来创建 EC2 环境,并在其中部署了一个 HelloWorld Web 应用程序。在自动化领域更进一步后,我们引入了Ansible配置管理工具。我们了解了 Ansible 如何处理应用程序部署和编排,以便 CloudFormation 模板在配置之前保持简洁和局限。这种方法在 AWS 云环境中被科技巨头们广泛接受,但当我们谈论具有多个云平台(如 AWS、Azure、Google Cloud、OpenStack 和 VMware)的异构环境时,作为 AWS 原生服务的 CloudFormation 就不再适用了。

因此,我们需要一个替代方案,不仅能够帮助我们提供计算服务,还能轻松地提供其他云原生服务。显然,这可以通过使用复杂且难以管理的命令式脚本来实现,但我们最终会使环境更加复杂。我们需要一个能够保持异构环境简单且可管理的解决方案,采用声明性方法,并遵循关于使用基础设施即代码IaC)的推荐指南。这个解决方案就是Terraform,它是一个安全高效地构建、修改和版本化基础设施的工具。

本章将涵盖以下主题:

  • 什么是 Terraform?

  • 创建一个 Terraform 仓库

  • 集成 AWS、Terraform 和 Ansible

技术要求

技术要求如下:

  • AWS 控制台

  • Git

  • GitHub

  • Terraform

  • Ansible

以下网站提供有关 Terraform 的更多信息:

什么是 Terraform?

Terraform 是一个开源的基础设施即代码(IaC)软件,于 2014 年 7 月由 HashiCorp 公司发布。该公司还生产了包括 Vagrant、Packer 和 Vault 等工具。Terraform 以 Mozilla 公共许可证MPL)2.0 版本发布。Terraform 的源代码可以在 GitHub 上找到:github.com/hashicorp/terraform。任何人都可以使用这些源代码并为 Terraform 的开发做出贡献。

Terraform 允许用户使用一种高级配置语言 HashiCorp 配置语言HCL)定义数据中心基础设施。HashiCorp 还提供了 Terraform 的企业版,带有额外的支持功能。Terraform 提供了许多功能,使其成为一个完美的高级基础设施编排工具。它具有以下特点:

  • 它的安装步骤非常简单,几乎不需要配置。

  • 它采用声明式的方法来编写 Terraform 模板。

  • 它既有开源版本,也有企业版。

  • 它具有幂等性,这意味着每次应用 Terraform 模板时,都会在你的环境中提供相同的结果。

  • 它几乎与所有主要的云平台完美兼容,如 AWS、Azure、GCP、OpenStack、DigitalOcean 等。更多详情请参阅www.terraform.io/docs/providers/

然而,Terraform 不是:

  • 配置管理工具,如 Puppet、Chef、Ansible 或 SaltStack。你可以安装一些轻量级程序或软件,将一些重要的配置文件推送到你的实例中,但当涉及到更复杂应用程序的部署和编排时,你需要使用前面章节中提到的配置工具。

  • 一个低级工具,如 AWS 的 Boto。

入门 Terraform

本书将重点介绍开源 Terraform。我们将展示如何在前几章中使用的 CentOS 7.x 机器上完整设置 Terraform。HashiCorp 不为操作系统提供原生软件包,因此 Terraform 作为单个二进制文件分发,并打包在 ZIP 压缩档案中。

让我们在 CentOS 服务器上设置 Terraform。请按照以下步骤操作:

  1. 我们必须从官方网站下载 Terraform 二进制文件:www.terraform.io/downloads.html。在我们的案例中,我们将使用 Linux 64 位版本:

  1. 解压提取的 Terraform .zip 文件。如果您的系统未安装 unzip 包,您需要先安装它:
$ yum -y install unzip
$ echo $PATH
$ unzip terraform_0.11.8_linux_amd64.zip -d /usr/bin/

这将把 Terraform 二进制文件提取到 /usr/bin 目录,Linux 系统的 PATH 环境变量中可以访问此目录。

  1. 最后,检查已安装的 Terraform 版本。编写时可用的最新版本 Terraform 软件如下:
$ terraform -v
Terraform v0.11.8

正如您所看到的,设置 Terraform 仅需几分钟,而且它的二进制文件非常轻量。现在我们已准备好使用 Terraform 环境进行 AWS 服务的配置。

使用 Terraform 和 AWS 进行自动化配置

如前所述,Terraform 支持多个提供商,如 AWS、Azure 和 GCP,进行高级基础设施编排。在本书中,我们仅使用 AWS 平台。正如我们在第二章中所看到的,部署您的第一个 Web 应用程序,我们可以使用两种模式来部署计算服务或任何 AWS 服务:

  • AWS 管理控制台

  • AWS 命令行界面CLI

使用 AWS 管理控制台进行部署

在这里,我们将重点介绍部署 AWS 计算服务,和之前的操作相同。使用 AWS 管理控制台部署 AWS 实例相对简单。请按照以下步骤进行:

  1. 登录到您的 AWS 管理控制台,访问 console.aws.amazon.com 或使用您的 IAM 用户账户登录。我们在第二章中创建了一个 IAM 用户账户,部署您的第一个 Web 应用程序,网址为 https://AWS-account-ID-or-alias.signin.aws.amazon.com/console

  2. 选择 "服务" 标签,接着选择 "计算" 部分中的 EC2,并点击 "Launch Instance" 按钮。

  3. 在下一屏幕上,搜索并选择 Amazon 机器映像AMI)。在本书中,我们使用的是 ami-cfe4b2b0,即 Amazon Linux AMI。

  4. 从 "选择实例类型" 步骤中选择 t2.micro 类型,并点击 "Next: Configure Instance Details" 按钮。

  5. 接受默认设置并点击 "Next: Add Storage" 按钮。

  6. 再次接受存储的默认设置,然后点击 "Next: Add tags" 按钮,接着点击 "Next: Configure Security Group" 按钮。

  7. 在此,选择您在第二章中创建的安全组,部署您的第一个 Web 应用程序,在我的案例中是 sg-01864b4c,如下图所示:

  1. 现在,点击 "Review and Launch" 按钮。忽略出现的任何警告并按 "Launch" 按钮。

  2. 选择密钥对,在我的案例中是 EffectiveDevOpsAWS。点击 "Launch Instances" 按钮。

几分钟后,你的 AWS 实例将启动并运行。服务器启动后,从本地实例(在我的例子中是 CentOS)登录到服务器。按照以下步骤手动部署 Hello World 应用程序并进行本地或浏览器验证:

$ ssh -i ~/.ssh/EffectiveDevOpsAWS.pem ec2-user@34.201.116.2 (replace this IP with your AWS public IP)
$ sudo yum install --enablerepo=epel -y nodejs
$ sudo wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.js -O /home/ec2-user/helloworld.js
$ sudo wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.conf -O /etc/init/helloworld.conf
$ sudo start helloworld
 $ curl http://34.201.116.2:3000/
Hello World

测试完成后,请记得通过 AWS 管理控制台终止该实例。

终止过程也非常简单。选择创建的实例,点击操作下拉菜单,选择实例状态选项,然后点击“终止”,如下图所示:

使用 AWS CLI 进行部署

使用 AWS CLI 创建实例并部署 Hello World Web 应用程序的步骤已经在第二章中演示过,部署你的第一个 Web 应用程序。你需要确保在继续之前安装 awscli 工具。以下是使用 AWS CLI 部署 Hello World Web 应用程序的简要概述:

$ aws ec2 run-instances \
 --instance-type t2.micro \
 --key-name EffectiveDevOpsAWS \
 --security-group-ids sg-01864b4c \
 --image-id ami-cfe4b2b0

$ aws ec2 describe-instances \
 --instance-ids i-0eb05adae2bb760c6 \
 --query "Reservations[*].Instances[*].PublicDnsName"

确保将 i-0eb05adae2bb760c6 替换为你在前一个命令中创建的 AWS 实例 ID。

$ ssh -i ~/.ssh/EffectiveDevOpsAWS.pem ec2-user@ec2-18-234-227-160.compute-1.amazonaws.com
$ sudo yum install --enablerepo=epel -y nodejs
$ sudo wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.js -O /home/ec2-user/helloworld.js
$ sudo wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.conf -O /etc/init/helloworld.conf
$ sudo start helloworld
 $ curl http://ec2-18-234-227-160.compute-1.amazonaws.com:3000/
 Hello World

测试完成后,请记得使用 aws ec2 terminate-instances --instance-ids <AWS 实例 ID> 终止该实例。

创建我们的 Terraform 仓库

现在我们已经看过了两种创建 AWS EC2 实例的方式:使用 AWS 管理控制台和使用 AWS CLI。我们可以通过 AWS 原生服务 CloudFormation 模板来实现自动化,如在第三章中所示,将基础设施视为代码。这仅适用于 AWS 云。在本章中,我们将使用 Terraform 达到相同的 AWS 实例配置效果。参考www.terraform.io/intro/vs/cloudformation.html以了解 Terraform 和 CloudFormation 的区别。

让我们在 GitHub 账户中创建一个专门的仓库,开始我们的 Terraform 之旅。登录 GitHub 后,按照以下步骤为 Terraform 模板创建一个新仓库:

  1. 在浏览器中,打开 github.com/new

  2. 如下截图所示,将新仓库命名为 EffectiveDevOpsTerraform

  1. 勾选“初始化此仓库并添加 README”复选框。

  2. 最后,点击“创建仓库”按钮。

  3. 创建完仓库后,你需要将其克隆到你的系统中。为此,你需要安装 Git。如果还没有安装 Git,可以在 Google 上搜索如何为你的操作系统安装它。对于 CentOS,你只需要运行 yum -y install git,因为 Git 包现在已经包含在 Linux 发行版中:

$ git clone https://github.com/<your_github_username>/EffectiveDevOpsTerraform

现在仓库已被克隆,接下来是开始开发 Terraform 模板。进入 EffectiveDevOpsTerraform 仓库并创建一个名为 firstproject 的目录:

$ cd EffectiveDevOpsTerraform
$ mkdir firstproject
$ cd firstproject

第一个用于 AWS 实例配置的 Terraform 模板

Terraform 用于创建、管理和更新基础设施资源,例如虚拟机、云实例、物理机器、容器等几乎所有基础设施类型都可以在 Terraform 中作为资源表示。我们将在下一步创建一个资源。在此之前,我们需要了解 Terraform 提供者,它们负责理解 API 交互并暴露资源。提供者可以是 IaaS(如 AWS、GCP 等)、PaaS(如 Heroku)或 SaaS(如 DNSimple)。提供者是我们必须从头开始编写 Terraform 模板的第一部分。在使用 Terraform 创建实例之前,我们需要配置 AWS 提供者。这将是我们在模板中编写的第一段代码。

模板是用一种叫做 HCL 的特殊语言编写的。关于 HCL 的更多细节可以在 github.com/hashicorp/hcl 找到。你也可以用 JSON 编写模板,但我们这里将使用 HCL。Terraform 模板文件必须以 .tf 为扩展名,代表 Terraform 文件。让我们创建第一个模板,ec2.tf

provider "aws" {
access_key = "<YOUR AWS ACCESS KEY>"
secret_key = "<YOUR AWS SECRET KEY>"
region = "us-east-1"
}

访问 www.terraform.io/docs/providers/aws/index.html 以探索更多关于 AWS 提供者的选项。

在 Terraform 中,声明提供者的这种方式叫做 使用静态凭证配置提供者。这并不是声明提供者的安全方式;Terraform 还有其他选项,比如环境变量、Terraform 变量文件、AWS 本地凭证文件(~/.aws/credentials)等,用来存储包含敏感信息的提供者。

不要将你的 AWS 访问密钥或密钥保存在 GitHub 或任何其他公共网站上。这样做将允许黑客攻击你的 AWS 账户。

在继续之前,我们需要安装或重新初始化与 AWS 相关的 Terraform 插件。我们不需要做太多,这个配置文件中的 provider 插件会为我们执行这个任务。

运行以下命令:

$ terraform init

上述命令的输出如下:

下一步是配置我们的基础设施。在这里,我们开始使用 Terraform 资源来开发 ec2.tf 文件。资源是基础设施的组件。它们可以像一个完整的虚拟服务器一样复杂,具有多个其他服务,或者像 DNS 记录一样简单。每个资源都属于一个提供者,资源类型的后缀是提供者的名称。资源的配置,也叫做 resource 块,具有以下形式:

resource "provider-name_resource-type" "resource-name" {
parameter_name = “parameter_value”
parameter_name = “parameter_value”
.
.
}

在我们的例子中,我们需要创建一个 EC2 实例。Terraform 中的 aws_instance 资源负责完成这项工作。为了创建实例,我们至少需要设置两个参数:amiinstance_type。这两个参数是必需的,而其他参数是可选的。为了获取所有 aws_instance 资源参数的列表和描述,请访问以下网站:www.terraform.io/docs/providers/aws/r/instance.html

在我们的例子中,我们将使用 AWS 管理控制台和 AWS CLI 工具创建并测试一个实例。我们使用 ami-cfe4b2b0 作为 AMI,t2.micro 作为实例类型。EffectiveDevOpsAWS 是我们之前创建的密钥名称,sg-01864b4c 是我们的安全组。我们还为实例添加了标签 helloworld,以便于识别。值得一提的是,像其他任何脚本或自动化语言一样,你可以在 Terraform 模板中使用 # 符号添加 注释。我们完整的文件现在应该如下所示:

# Provider Configuration for AWS
provider "aws" {
access_key = “<YOUR AWS ACCESS KEY>"
secret_key = "<YOUR AWS SECRET KEY>"
region = "us-east-1"
}

# Resource Configuration for AWS
resource "aws_instance" "myserver" {
ami = "ami-cfe4b2b0"
instance_type = "t2.micro"
key_name = "EffectiveDevOpsAWS"
vpc_security_group_ids = ["sg-01864b4c"]
tags {
Name = "helloworld"
}
}

创建的文件应该像以下网站中的文件一样:raw.githubusercontent.com/yogeshraheja/EffectiveDevOpsTerraform/master/firstproject/ec2.tf

让我们先验证 Terraform 模板,确保模板没有语法错误。Terraform 提供了一个专门的 terraform validate 工具,它检查 Terraform 模板的语法,并在发现需要注意的语法错误时给出输出:

$ terraform validate

由于没有输出,这意味着我们的 Terraform 模板没有语法错误。是时候进行一次干运行,以查看这个模板将执行什么操作。这只是一个烟雾测试,用来找出模板将执行哪些更改或实现。Terraform 中的这个步骤称为 plan

[root@yogeshraheja firstproject]# terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
 + create

Terraform will perform the following actions:

 + aws_instance.myserver
 id: <computed>
 ami: "ami-cfe4b2b0"
 arn: <computed>
 associate_public_ip_address: <computed>
 availability_zone: <computed>
 cpu_core_count: <computed>
 cpu_threads_per_core: <computed>
 ebs_block_device.#: <computed>
 ephemeral_block_device.#: <computed>
 get_password_data: "false"
 instance_state: <computed>
 instance_type: "t2.micro"
 ipv6_address_count: <computed>
 ipv6_addresses.#: <computed>
 key_name: "EffectiveDevOpsAWS"
 network_interface.#: <computed>
 network_interface_id: <computed>
 password_data: <computed>
 placement_group: <computed>
 primary_network_interface_id: <computed>
 private_dns: <computed>
 private_ip: <computed>
 public_dns: <computed>
 public_ip: <computed>
 root_block_device.#: <computed>
 security_groups.#: <computed>
 source_dest_check: "true"
 subnet_id: <computed>
 tags.%: "1"
 tags.Name: "helloworld"
 tenancy: <computed>
 volume_tags.%: <computed>
 vpc_security_group_ids.#: "1"
 vpc_security_group_ids.1524136243: "sg-01864b4c"

Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

在这里,我们没有指定 -out 参数来保存这个计划,因此 Terraform 无法保证如果后续运行 terraform apply 时,这些操作将会 完全 执行。

[root@yogeshraheja firstproject]#

我们的计划阶段指示了我们在实际执行时希望创建实例的相同参数。再次强调,不要混淆 <computed> 参数,这只是表示它们的值将在资源创建时分配。

现在让我们真正执行我们的计划,看看如何使用 Terraform 模板创建一个具有定义资源参数的 AWS 实例。Terraform 使用 terraform apply 工具来执行此操作,你可以把这个阶段看作是 apply。一旦执行 terraform apply,它默认会要求你确认操作。输入 yes 来开始资源创建。

如果你想在应用计划之前跳过此交互式审批,可以在terraform apply命令中使用--auto-approve选项:

[root@yogeshraheja firstproject]# terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.myserver
      id: <computed>
      ami: "ami-cfe4b2b0"
      arn: <computed>
      associate_public_ip_address: <computed>
      availability_zone: <computed>
      cpu_core_count: <computed>
      cpu_threads_per_core: <computed>
      ebs_block_device.#: <computed>
      ephemeral_block_device.#: <computed>
      get_password_data: "false"
      instance_state: <computed>
      instance_type: "t2.micro"
      ipv6_address_count: <computed>
      ipv6_addresses.#: <computed>
      key_name: "EffectiveDevOpsAWS"
      network_interface.#: <computed>
      network_interface_id: <computed>
      password_data: <computed>
      placement_group: <computed>
      primary_network_interface_id: <computed>
      private_dns: <computed>
      private_ip: <computed>
      public_dns: <computed>
      public_ip: <computed>
      root_block_device.#: <computed>
      security_groups.#: <computed>
      source_dest_check: "true"
      subnet_id: <computed>
      tags.%: "1"
      tags.Name: "helloworld"
      tenancy: <computed>
      volume_tags.%: <computed>
      vpc_security_group_ids.#: "1"
      vpc_security_group_ids.1524136243: "sg-01864b4c"

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_instance.myserver: Creating...
  ami: "" => "ami-cfe4b2b0"
  arn: "" => "<computed>"
  associate_public_ip_address: "" => "<computed>"
  availability_zone: "" => "<computed>"
  cpu_core_count: "" => "<computed>"
  cpu_threads_per_core: "" => "<computed>"
  ebs_block_device.#: "" => "<computed>"
  ephemeral_block_device.#: "" => "<computed>"
  get_password_data: "" => "false"
  instance_state: "" => "<computed>"
  instance_type: "" => "t2.micro"
  ipv6_address_count: "" => "<computed>"
  ipv6_addresses.#: "" => "<computed>"
  key_name: "" => "EffectiveDevOpsAWS"
  network_interface.#: "" => "<computed>"
  network_interface_id: "" => "<computed>"
  password_data: "" => "<computed>"
  placement_group: "" => "<computed>"
  primary_network_interface_id: "" => "<computed>"
  private_dns: "" => "<computed>"
  private_ip: "" => "<computed>"
  public_dns: "" => "<computed>"
  public_ip: "" => "<computed>"
  root_block_device.#: "" => "<computed>"
  security_groups.#: "" => "<computed>"
  source_dest_check: "" => "true"
  subnet_id: "" => "<computed>"
  tags.%: "" => "1"
  tags.Name: "" => "helloworld"
  tenancy: "" => "<computed>"
  volume_tags.%: "" => "<computed>"
  vpc_security_group_ids.#: "" => "1"
  vpc_security_group_ids.1524136243: "" => "sg-01864b4c"
aws_instance.myserver: Still creating... (10s elapsed)
aws_instance.myserver: Still creating... (20s elapsed)
aws_instance.myserver: Creation complete after 22s (ID: i-dd8834ca)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
[root@yogeshraheja firstproject]# 

让我们在 AWS 控制台中确认新创建的实例,确保helloworld实例是由 Terraform 模板创建的:

Terraform 不仅仅创建了一个实例然后就忘记了它。实际上,Terraform 会将它所知道的关于资源(在我们的例子中是实例)的所有信息保存到一个特殊的文件中,这个文件在 Terraform 中被称为状态文件。在这个文件中,Terraform 存储了它所创建的所有资源的状态。它保存在与 Terraform 模板相同的目录中,并且文件扩展名为.tfstate。状态文件的格式是一个简单的 JSON 格式:

[root@yogeshraheja firstproject]# cat terraform.tfstate
{
 "version": 3,
 "terraform_version": "0.11.8",
 "serial": 1,
 "lineage": "9158b0ed-754a-e01e-094e-6b0827347950",
 "modules": [
 {
 "path": [
 "root"
 ],
 "outputs": {},
 "resources": {
 "aws_instance.myserver": {
 "type": "aws_instance",
 "depends_on": [],
 "primary": {
 "id": "i-dd8834ca",
 "attributes": {
 "ami": "ami-cfe4b2b0",
 "arn": "arn:aws:ec2:us-east-1:094507990803:instance/i-dd8834ca",
 "associate_public_ip_address": "true",
 "availability_zone": "us-east-1b",
 "cpu_core_count": "1",
 "cpu_threads_per_core": "1",
 "credit_specification.#": "1",
 "credit_specification.0.cpu_credits": "standard",
 "disable_api_termination": "false",
 "ebs_block_device.#": "0",
 "ebs_optimized": "false",
 "ephemeral_block_device.#": "0",
 "get_password_data": "false",
 "iam_instance_profile": "",
 "id": "i-dd8834ca",
 "instance_state": "running",
 "instance_type": "t2.micro",
 "ipv6_addresses.#": "0",
 "key_name": "EffectiveDevOpsAWS",
 "monitoring": "false",
 "network_interface.#": "0",
 "network_interface_id": "eni-b0683ee7",
 "password_data": "",
 "placement_group": "",
 "primary_network_interface_id": "eni-b0683ee7",
 "private_dns": "ip-172-31-74-203.ec2.internal",
 "private_ip": "172.31.74.203",
 "public_dns": "ec2-52-70-251-228.compute-1.amazonaws.com",
 "public_ip": "52.70.251.228",
 "root_block_device.#": "1",
 "root_block_device.0.delete_on_termination": "true",
 "root_block_device.0.iops": "100",
 "root_block_device.0.volume_id": "vol-024f64aa1bb805237",
 "root_block_device.0.volume_size": "8",
 "root_block_device.0.volume_type": "gp2",
 "security_groups.#": "1",
 "security_groups.2004290681": "HelloWorld",
 "source_dest_check": "true",
 "subnet_id": "subnet-658b6149",
 "tags.%": "1",
 "tags.Name": "helloworld",
 "tenancy": "default",
 "volume_tags.%": "0",
 "vpc_security_group_ids.#": "1",
 "vpc_security_group_ids.1524136243": "sg-01864b4c"
 },
 "meta": {
 "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0": {
 "create": 600000000000,
 "delete": 1200000000000,
 "update": 600000000000
 },
 "schema_version": "1"
 },
 "tainted": false
 },
 "deposed": [],
 "provider": "provider.aws"
 }
 },
 "depends_on": []
 }
 ]
}
[root@yogeshraheja firstproject]#

Terraform 的特殊之处在于,你可以通过terraform show命令以人类可读的格式读取这个 JSON 输出:

[root@yogeshraheja firstproject]# terraform show
aws_instance.myserver:
 id = i-dd8834ca
 ami = ami-cfe4b2b0
 arn = arn:aws:ec2:us-east-1:094507990803:instance/i-dd8834ca
 associate_public_ip_address = true
 availability_zone = us-east-1b
 cpu_core_count = 1
 cpu_threads_per_core = 1
 credit_specification.# = 1
 credit_specification.0.cpu_credits = standard
 disable_api_termination = false
 ebs_block_device.# = 0
 ebs_optimized = false
 ephemeral_block_device.# = 0
 get_password_data = false
 iam_instance_profile =
 instance_state = running
 instance_type = t2.micro
 ipv6_addresses.# = 0
 key_name = EffectiveDevOpsAWS
 monitoring = false
 network_interface.# = 0
 network_interface_id = eni-b0683ee7
 password_data =
 placement_group =
 primary_network_interface_id = eni-b0683ee7
 private_dns = ip-172-31-74-203.ec2.internal
 private_ip = 172.31.74.203
 public_dns = ec2-52-70-251-228.compute-1.amazonaws.com
 public_ip = 52.70.251.228
 root_block_device.# = 1
 root_block_device.0.delete_on_termination = true
 root_block_device.0.iops = 100
 root_block_device.0.volume_id = vol-024f64aa1bb805237
 root_block_device.0.volume_size = 8
 root_block_device.0.volume_type = gp2
 security_groups.# = 1
 security_groups.2004290681 = HelloWorld
 source_dest_check = true
 subnet_id = subnet-658b6149
 tags.% = 1
 tags.Name = helloworld
 tenancy = default
 volume_tags.% = 0
 vpc_security_group_ids.# = 1
 vpc_security_group_ids.1524136243 = sg-01864b4c

[root@yogeshraheja firstproject]#

到目前为止,我们已经创建了一个 Terraform 模板,验证它以确保没有语法错误,并进行了terraform plan形式的冒烟测试,最后通过terraform apply应用了我们的 Terraform 模板来创建资源。

剩下的问题是,我们如何删除或销毁 Terraform 模板创建的所有资源?我们是否需要逐一查找并删除资源?答案是否定的,Terraform 会自动处理这个问题。通过参考 Terraform 在apply阶段创建的状态文件,任何由 Terraform 创建的资源都可以使用简单的terraform destroy命令在template目录中销毁:

[root@yogeshraheja firstproject]# terraform destroy
aws_instance.myserver: Refreshing state... (ID: i-dd8834ca)

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
 - destroy

Terraform will perform the following actions:

 - aws_instance.myserver

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
 Terraform will destroy all your managed infrastructure, as shown above.
 There is no undo. Only 'yes' will be accepted to confirm.

 Enter a value: yes

aws_instance.myserver: Destroying... (ID: i-dd8834ca)
aws_instance.myserver: Still destroying... (ID: i-dd8834ca, 10s elapsed)
aws_instance.myserver: Still destroying... (ID: i-dd8834ca, 20s elapsed)
aws_instance.myserver: Still destroying... (ID: i-dd8834ca, 30s elapsed)
aws_instance.myserver: Still destroying... (ID: i-dd8834ca, 40s elapsed)
aws_instance.myserver: Still destroying... (ID: i-dd8834ca, 50s elapsed)
aws_instance.myserver: Destruction complete after 1m0s

Destroy complete! Resources: 1 destroyed.
[root@yogeshraheja firstproject]#

检查你的 AWS 控制台,确保实例处于终止状态。

现在检查terraform show命令。由于没有任何资源可用,它应该为空。

用于部署 Hello World 应用程序的第二个 Terraform 模板

进入 EffectiveDevOpsTerraform 仓库并创建一个名为second project的目录:

$ mkdir secondproject
$ cd secondproject

现在,我们已经在上一节中使用 Terraform 模板创建了 EC2 实例,接下来我们准备扩展我们 Hello World 网页应用程序的配置。我们将使用Terraform Provisioner重新创建我们之前通过第二章中 CloudFormation 的UserDatablock字段,部署你的第一个网页应用以及在第三章中使用 Ansible 角色,将基础设施视为代码时所创建的 Hello World 堆栈。如果你还记得,UserData字段大致如下所示:

yum install --enablerepo=epel -y nodejs
wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.js -O /home/ec2-user/helloworld.js
wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.conf -O /etc/init/helloworld.conf
start helloworld

您会观察到,部署我们 Hello World 网络应用程序的操作有三种不同的类型。首先,我们准备系统以运行应用程序。在我们的示例中,我们只是安装 Node.js 包。接下来,我们复制运行应用程序所需的不同资源。在我们的例子中,这些资源包括 JavaScript 代码和 upstart 配置。最后,我们启动服务。

为了部署我们的 Hello World 网络应用程序,我们需要引入Terraform Provisioner。Terraform 中的 Provisioners 是可用于多个资源的配置块,允许您在资源创建后执行操作。它主要用于 EC2 实例。Provisioners 主要用作构建后步骤,用于安装轻量级应用程序或配置管理代理,例如Puppet 代理chef-client。它们甚至可以用来运行配置管理工具,如playbooksPuppet 模块Chef 食谱Salt 配方。在接下来的章节中,我们将查看如何将 Terraform 与 Ansible 配合使用的几个示例。

让我们创建helloworldec2.tf Terraform 模板来创建实例,然后引入provisioner块,使用remote-exec与新创建的实例建立连接,并在其上下载和部署 Hello World 应用程序。我们完成的 Terraform 模板应如下所示:

# Provider Configuration for AWS
provider "aws" {
  access_key = "<YOUR AWS ACCESS KEY>"
  secret_key = "<YOUR AWS SECRET KEY>"
  region = "us-east-1"
}

# Resource Configuration for AWS
resource "aws_instance" "myserver" {
  ami = "ami-cfe4b2b0"
  instance_type = "t2.micro"
  key_name = "EffectiveDevOpsAWS"
  vpc_security_group_ids = ["sg-01864b4c"]

  tags {
    Name = "helloworld"
  }

# Helloworld Appication code
  provisioner "remote-exec" {
    connection {
      user = "ec2-user"
      private_key = "${file("/root/.ssh/EffectiveDevOpsAWS.pem")}"
    }
    inline = [
      "sudo yum install --enablerepo=epel -y nodejs",
      "sudo wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.js -O /home/ec2-user/helloworld.js",
      "sudo wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.conf -O /etc/init/helloworld.conf",
      "sudo start helloworld",
    ]
  }
}

创建的文件应如下所示:raw.githubusercontent.com/yogeshraheja/EffectiveDevOpsTerraform/master/secondproject/helloworldec2.tf

由于我们正在新目录secondproject中创建 Terraform 模板,因此需要安装插件或重新初始化与 AWS 相关的 Terraform 插件。配置了provider部分的文件将为我们执行此任务:

$ terraform init

现在,到了验证 Terraform 模板文件的时间,确保它没有任何语法错误。验证成功后,运行plan命令,然后使用terraform apply命令完整执行模板:

$ terraform validate
$ terraform plan
$ terraform apply

我们将得到以下输出:

我们的 Terraform 模板已经成功执行。我们已配置好 EC2 实例并部署了 Hello World 网络应用程序。通过执行terraform show命令,接着运行curl命令,我们可以找到实例的公网 IP,并确保应用程序已正确部署:

$ terraform show | grep -i public_ip
$ curl <PUBLIC_IP>:3000

执行前面命令的输出如下:

让我们也从浏览器验证应用程序的输出,如下截图所示:

我们已经成功地使用 Terraform 部署了我们的 Hello World 网页应用。测试完成后,确保在继续到下一章节之前,删除所有已创建的资源。执行 terraform destroy 命令,它将通过引用 Terraform 状态文件来删除所有已创建的资源。

运行以下命令:

$ terraform destroy

集成 AWS、Terraform 和 Ansible

在前面的章节中,我们学习了如何使用 Terraform 配置一个基础实例。接着,我们学习了如何使用 Terraform 配置一个基础的 EC2 实例,并通过 post builds 执行构建操作。现在,我们将学习如何将 Terraform 与 Ansible 集成,以执行配置管理任务。我们将考虑两种不同的场景。在第一个场景中,我们将配置一个 EC2 实例,并使用 push 模式运行 Ansible,这是我们使用 Ansible 执行自动化的主要方式。在第二个场景中,我们将配置一个 EC2 实例,并使用 ansible pull 方法在 pull 模式下运行 Ansible。

使用推送模式的 Terraform 和 Ansible

进入 EffectiveDevOpsTerraform 仓库,创建一个名为 thirdproject 的目录:

$ mkdir thirdproject
$ cd thirdproject

在这个例子中,我们将遵循推荐的实践来创建 Terraform 模板。首先,我们将从 Terraform 模板中移除我们的 AWS access_key 和 AWS secret_key。我们系统中已安装 AWS CLI,这意味着我们已经配置了该系统与我们的 AWS 账户进行通信。如果我们还没有安装 AWS CLI,我们将使用 aws configure 来安装它。安装后,它将在 /root/.aws 目录中创建一个 credentials 文件,其中包含我们的 AWS 访问密钥和秘密密钥。我们将利用该文件,在 Terraform 模板中使用相同的凭证在 AWS 账户上构建资源:

[root@yogeshraheja thirdproject]# cat /root/.aws/credentials
[default]
aws_access_key_id = <YOUR AWS SECRET KEY>
aws_secret_access_key = <YOUR AWS SECRET KEY>
[root@yogeshraheja thirdproject]#

现在是时候开始编写我们的 helloworldansible.tf Terraform 模板了。在这个例子中,我们将配置一个 EC2 实例,并通过使用 remote-exec provisioner 验证连接来等待 SSH 服务的出现。然后,我们将使用 local-exec provisioner 创建一个包含新 IP 地址的清单,并通过执行 ansible-playbook 命令在本地系统上使用主要的推送模型运行 Ansible 剧本。

在 provisioners(仅在 provisioners 内)中,我们可以使用一个特殊的关键字 self 来访问正在配置的资源的属性。

我们在代码中还使用了一个叫做 output 的块。输出块允许你在 Terraform 模板应用后,通过 Terraform output 命令返回数据:

# Provider Configuration for AWS
provider "aws" {
  region = "us-east-1"
}

# Resource Configuration for AWS
resource "aws_instance" "myserver" {
  ami = "ami-cfe4b2b0"
  instance_type = "t2.micro"
  key_name = "EffectiveDevOpsAWS"
  vpc_security_group_ids = ["sg-01864b4c"]

  tags {
    Name = "helloworld"
  }

# Provisioner for applying Ansible playbook
  provisioner "remote-exec" {
    connection {
      user = "ec2-user"
      private_key = "${file("/root/.ssh/EffectiveDevOpsAWS.pem")}"
    }
  }

  provisioner "local-exec" {
    command = "sudo echo '${self.public_ip}' > ./myinventory",
  }

  provisioner "local-exec" {
    command = "sudo ansible-playbook -i myinventory --private-key=/root/.ssh/EffectiveDevOpsAWS.pem helloworld.yml",
  } 
}

# IP address of newly created EC2 instance
output "myserver" {
 value = "${aws_instance.myserver.public_ip}"
}

创建的文件应类似于以下文件:raw.githubusercontent.com/yogeshraheja/EffectiveDevOpsTerraform/master/thirdproject/helloworldansible.tf

我们将在helloworld.yml Ansible playbook 中调用helloworld角色来部署 Hello World web 应用程序:

---
- hosts: all
  become: yes
  roles:
    - helloworld

Ansible 配置文件ansible.cfg应如下所示。它应该指向我们thirdproject目录结构中的myinventory文件:

[defaults]
inventory = $PWD/myinventory
roles_path = ./roles
remote_user = ec2-user
become = True
become_method = sudo
become_user = root
nocows = 1
host_key_checking = False

完整的项目应如下所示:github.com/yogeshraheja/EffectiveDevOpsTerraform/tree/master/thirdproject

由于我们创建了一个新的目录thirdproject,我们需要重新安装与 AWS 相关的插件,或者重新初始化与 Terraform 相关的插件。带有provider部分的配置文件将为我们执行此任务:

$ terraform init

现在是验证 Terraform 模板文件的时候了,以确保它没有语法错误。验证成功后,执行计划并使用terraform apply进行实际运行:

$ terraform validate
$ terraform plan
$ terraform apply

输出清晰地显示了 Ansible playbook 的日志,并返回带有公网 IP 的output块。让我们使用这个公网 IP 来验证应用程序部署:

$ curl 54.85.107.87:3000

运行前面的命令后的输出如下所示:

让我们从浏览器验证应用程序输出,如下图所示:

部署成功后,执行terraform destroy以清理已创建的资源:

$ terraform destroy

使用 Ansible 的拉取方式的 Terraform

进入EffectiveDevOpsTerraform仓库,并创建一个名为fourthproject的目录:

$ mkdir fourthproject
$ cd fourthproject

我们将在这里再次遵循 Terraform 模板的最佳实践,并使用位于/root/.aws目录中的credentials文件,该文件包含我们的 AWS 访问密钥和秘密密钥。在这种情况下,我们将以反向方式使用 Ansible:Ansible 拉取方式。要使用这种反向方式的 Ansible,我们必须确保在已提供的 EC2 实例上安装 Ansible,并通过引用源代码仓库中存在的 Ansible 代码来运行ansible-pull

在我们的案例中,我们将使用在第三章中创建的相同 Ansible 代码,将基础设施视为代码,该代码位于github.com/yogeshraheja/ansible。在我们的helloworldansiblepull.tf Terraform 模板中,我们将使用remote-exec Terraform 提供程序与新创建的实例建立连接。我们将使用inline属性在新创建的 EC2 实例上远程执行多个命令。我们的 Terraform 模板应如下所示:

# Provider Configuration for AWS
provider "aws" {
  region = "us-east-1"
}

# Resource Configuration for AWS
resource "aws_instance" "myserver" {
  ami = "ami-cfe4b2b0"
  instance_type = "t2.micro"
  key_name = "EffectiveDevOpsAWS"
  vpc_security_group_ids = ["sg-01864b4c"]

  tags {
    Name = "helloworld"
  }

# Provisioner for applying Ansible playbook in Pull mode
  provisioner "remote-exec" {
    connection {
      user = "ec2-user"
      private_key = "${file("/root/.ssh/EffectiveDevOpsAWS.pem")}"
    }
    inline = [
      "sudo yum install --enablerepo=epel -y ansible git",
      "sudo ansible-pull -U https://github.com/yogeshraheja/ansible helloworld.yml -i localhost",
    ]
  }

}

# IP address of newly created EC2 instance
output "myserver" {
 value = "${aws_instance.myserver.public_ip}"
}

创建的文件应与以下文件相似:raw.githubusercontent.com/yogeshraheja/EffectiveDevOpsTerraform/master/fourthproject/helloworldansiblepull.tf.

由于我们刚刚创建了一个新的目录fourthproject,我们需要为 Terraform 安装插件或重新初始化与 AWS 相关的插件。包含provider部分的配置文件将为我们执行此任务。

$ terraform init

现在是时候验证 Terraform 模板文件,确保没有语法错误。成功验证后,执行计划并使用terraform apply进行实际运行:

$ terraform validate
$ terraform plan
$ terraform apply

如预期所示,Ansible 代码已在新创建的 EC2 实例上本地运行。Terraform 模板中配置的output块也返回了期望的公共 IP 值。我们通过curl命令来验证输出:

$ curl 18.212.64.84:3000/

执行前述命令的输出如下:

最后,从浏览器验证输出,如下图所示:

很好——应用程序已部署并通过验证。一旦完成,别忘了使用以下命令销毁资源,以避免不必要的 AWS 账单:

$ terraform destroy

总结

在本章中,我们学习了如何使用 Terraform 模板高效管理基础设施。首先,我们了解了如何使用 Terraform 仅通过几行代码来配置 EC2 实例。接着,我们研究了如何使用 Terraform 提供者(provisioners)创建模板以部署轻量级应用程序。然后,我们通过 Ansible 扩展了 Terraform 模板,这使我们能够协调部署 Web 应用程序的不同步骤。最后,我们探讨了如何将 Terraform 与 Ansible 结合使用,以拉取方式进行集成,这在管理大规模基础设施时通常更为合理,正如我们在第三章,将基础设施视为代码中所观察到的那样。

我们现在有了一个良好的生产环境,准备托管任何应用程序。我们已经了解了如何使用 CloudFormation、Ansible 和 Terraform 来架构它。在第五章,添加持续集成与持续部署中,我们将继续使用 CloudFormation 和 Ansible,但在软件交付的背景下,因为我们将学习如何实施持续集成测试和持续部署。

问题

  1. 什么是 Terraform,它与其他配置管理工具有何不同?

  2. 如何在基于 Linux 的操作系统上安装 Terraform?

  3. 如何使用 Terraform 模板配置你的第一个 AWS 实例?

  4. 如何编写 Terraform 模板以集成基于拉取的 Ansible 方法?

深入阅读

阅读以下文章以获取更多信息:

第五章:添加持续集成和持续部署

在前几章中,我们集中讨论了改善基础设施的创建和管理。然而,DevOps 文化并不止于此。如你从第一章《云计算与 DevOps 革命》中所记得的,DevOps 文化还包括拥有一个非常高效的代码测试和部署流程。在 2009 年 Velocity 大会上,John Allspaw 和 Paul Hammond 做了一个非常鼓舞人心的演讲,介绍了 Flickr 是如何每天进行超过 10 次部署的(bit.ly/292ASlW)。这次演讲常被提及为 DevOps 运动诞生的关键时刻之一。在他们的演讲中,John 和 Paul 谈到了开发和运维团队之间的冲突,同时也概述了一些最佳实践,使得 Flickr 能够每天多次将新代码部署到生产环境。

随着虚拟化、公共云和私有云以及自动化等创新的出现,创建新公司从未如此容易。因此,许多公司现在面临的最大问题是如何脱颖而出,区别于竞争对手。比大多数竞争者更快地迭代能力可能对公司的成功至关重要。一个高效的 DevOps 组织利用多种工具和策略,以提高工程团队将新代码发布到生产环境的速度。这正是我们将在本章中关注的内容。

我们将首先着手创建一个持续集成CI)流水线。CI 流水线将允许我们自动且持续地测试提议的代码更改。这将释放开发人员和质量保证人员的时间,他们将不再需要进行大量的手动测试。它还使得代码更改的集成变得更加容易。为了实现我们的流水线,我们将使用 GitHub 和一个最广泛使用的集成工具——Jenkins

然后我们将探讨如何创建一个持续部署CD)流水线。一旦代码通过了 CI 流水线,我们将使用这个持续部署流水线自动部署新代码。我们将依赖两个 AWS 服务来实现这个流水线——AWS CodeDeployAWS CodePipeline。CodeDeploy 让我们定义如何在 EC2 实例上部署新代码,而 CodePipeline 则让我们可以协调应用程序的整个生命周期。

为了将我们的代码部署到生产环境,我们将增加一个额外的步骤,允许操作员通过按下一个按钮将最新的构建版本从预发布环境部署到生产环境。这种按需将代码部署到生产环境的能力被称为 CD(持续部署)。它的主要优势在于,它允许部署操作员在将代码部署到生产环境之前,在预发布环境中验证构建。章节末尾,我们将介绍一些有效的工程组织使用的技术和策略,帮助将持续交付管道转变为持续部署管道,从而使整个代码部署过程无需人工干预即可完成。我们将讨论以下主题:

  • 构建持续集成管道

  • 构建持续部署管道

技术要求

本章节的技术要求如下:

  • GitHub

  • Jenkins

  • Ansible

  • AWS CodeDeploy

  • AWS CodePipeline

相关链接如下:

构建 CI 管道

最初,在 CI 环境中工作意味着开发人员必须尽可能频繁地将代码提交到一个公共分支,而不是在一个单独的分支上工作或几周不提交更改。这可以提高当前工作的可见性,并促进沟通,避免集成问题,这种情况通常被称为 集成地狱。随着源代码管理、构建和发布管理相关工具集的成熟,理想世界中代码集成的愿景也逐渐明确。

如今,大多数有效的工程组织将继续沿着早期频繁集成的道路前进。然而,他们通常使用一种更现代的开发过程,在这种过程中,开发人员需要在编辑代码的同时,添加或编辑相关的测试以验证更改。这大大提高了整体生产力;现在更容易发现新错误,因为每次合并时代码的变化量相对较小。

要采用这样的工作流程,例如使用 Git 这样的源代码管理工具,你可以按以下步骤进行:

  1. 作为开发者,当你想做出更改时,首先创建一个新的 Git 分支,从主分支的 HEAD 分支出。

  2. 编辑代码,并同时添加或编辑不同的相关测试以验证更改。

  3. 在本地测试代码。

  4. 当代码准备好时,将分支 rebase 以集成其他开发者的最新更改。如有需要,解决冲突并再次测试代码。

  5. 如果一切顺利,下一步是创建一个pull request。在这个过程中,你需要告诉其他开发者你的代码已经准备好进行审查。

  6. 一旦创建了 pull request,一个自动化测试系统(如我们将在本章中构建的系统)将捕捉到更改并运行整个测试套件,以确保没有任何失败。

  7. 此外,其他相关方将审查代码以及添加到分支中的不同测试。如果他们对所提议的更改满意,他们将批准它,允许开发者合并他们的更改。

  8. 在最后一步,开发者合并他们的 pull requests,这将意味着将他们的新代码合并并测试主分支。其他开发者现在将在重新 base 或创建新分支时集成这个更改。

在接下来的部分中,我们将创建一个使用 Jenkins 的 CI 服务器,运行在 EC2 实例和 GitHub 之上。

随着项目的扩大,测试的数量和运行测试所需的时间也会增加。虽然像 Bazel (bazel.build/) 这样的高级构建系统能够只运行与特定更改相关的测试,但通常更容易从简单开始,创建一个每次提交新 pull request 时都会运行所有可用测试的 CI 系统。拥有像 AWS 这样弹性的外部测试基础设施,对于那些不希望等待几分钟甚至几小时来执行所有测试的开发者来说,将节省大量时间。在本书中,我们将重点讨论 Web 应用开发。你可能会面临一个更加具有挑战性的环境,需要为特定的硬件和操作系统构建软件。拥有一个专门的 CI 系统,将允许你在最终目标的硬件和软件上运行测试。

使用 Ansible 和 CloudFormation 创建 Jenkins 服务器

如前所述,我们将使用 Jenkins 作为我们运行 CI 流水线的核心系统。Jenkins 有超过 10 年的开发历史,长期以来一直是实践持续集成的领先开源解决方案。Jenkins 以其丰富的插件生态系统而闻名,已经经历了一个重大的新版本发布(Jenkins 2.x),这使得它更加聚焦于一些 DevOps 中心的功能,包括能够创建可以进行版本控制和检查的原生交付流水线。它还提供了与源代码管理系统(如我们在本书中使用的 GitHub)的更好集成。

我们将继续像在第三章中一样使用 AnsibleCloudFormation将你的基础设施视为代码,来管理我们的 Jenkins 服务器。

为 Jenkins 创建 Ansible playbook

首先,导航到我们的 ansible 角色目录:

$ cd ansible/roles  

该目录应包含 helloworldnodejs 目录,以及我们在第三章中创建的配置,将你的基础设施视为代码。我们现在将使用 ansible-galaxy 命令创建我们的 Jenkins 角色:

$ ansible-galaxy init jenkins  

现在,我们将通过编辑文件 jenkins/tasks/main.yml 来编辑该新角色的任务定义。用你喜欢的文本编辑器打开文件。

我们任务的目标是安装并启动 Jenkins。为了实现这一目标,由于我们使用的是基于 Linux 的操作系统(在我们的例子中是 AWS Amazon Linux),我们将通过 yum 安装一个 RPM 包。Jenkins 维护了一个 yum 仓库,所以第一步将是将其导入到我们的 yum 仓库配置中,基本上是作为 /etc/yum.repos.d 中的一个条目:

以下是任务文件的初始注释,请添加以下内容:

- name: Add Jenkins repository
  shell: wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat/jenkins.repo

下一步将是导入该仓库的 GPG 密钥。Ansible 有一个模块来管理这些密钥:

- name: Import Jenkins GPG key 
  rpm_key: 
    state: present 
    key: https://pkg.jenkins.io/redhat/jenkins.io.key

我们现在已经到达了可以使用 yum 安装 Jenkins 的步骤。我们将通过以下调用来完成:

- name: Install Jenkins 
  yum: 
    name: jenkins-2.99
    enablerepo: jenkins 
    state: present

由于 jenkins 仓库默认被禁用,我们通过 enablerepo 标志在执行此 yum 命令时启用它。

此时,Jenkins 已经安装完成。为了遵循最佳实践指南,我们将指定要安装的 Jenkins 版本(在我们的例子中是 2.99)。我们还希望启动该服务并在 chkconfig 级别启用它,这样如果安装了 Jenkins 的 EC2 实例重新启动,Jenkins 会自动启动。我们可以通过使用服务模块来做到这一点。请在前一个调用后添加以下内容:

- name: Start Jenkins 
  service: 
    name: jenkins 
    enabled: yes 
    state: started 

对于一个简单的 Jenkins 角色,这就是我们所需要的。

我们现在应该有一个 main.yml 文件,如下所示:raw.githubusercontent.com/yogeshraheja/ansible/master/roles/jenkins/tasks/main.yml

AWS Amazon Linux 配备了 Java 7,但 Jenkins 在版本 2.54 及以上要求安装 Java 8。因此,在上面的链接中你会看到两个额外的任务,这些任务将卸载 Java 7 并安装 Java 8:

- name: Removing old version of JAVA from Amazon Linux
  yum:
    name: java-1.7.0-openjdk
    state: absent

- name: Install specific supported version of JAVA
  yum:
    name: java-1.8.0-openjdk
    state: present

随着你对 Jenkins 和 Ansible 的经验积累,探索网络或 Ansible Galaxy,你会发现更多的高级角色,允许你更详细地配置 Jenkins、生成作业并选择要安装的插件。这是一个重要的步骤,本书不会涵盖,但理想情况下,你希望你的整个系统都能通过代码来描述。此外,在本章中,我们使用的是 HTTP 协议上的 Jenkins。强烈建议使用加密协议(如 HTTPS),或者正如我们将在第八章《强化 AWS 环境的安全性》中看到的那样,使用 VPN 连接的私有子网。

我们现在已经构建了一个角色,使我们能够安装 Jenkins。我们希望创建一个新的 EC2 实例并在其上安装 Jenkins,最终目标是在该实例上测试我们的 Node.js 代码。为了实现这一点,Jenkins 主机还需要安装 Node 和 npm

我们有两种选择。我们可以像为 helloworld 角色做的那样,将我们的 nodejs 角色作为 Jenkins 角色的依赖项,或者我们可以将 nodejs 角色列在我们的剧本角色列表中。由于最终 Jenkins 并不需要 Node 才能运行,我们将选择第二种方法。在我们 ansible 仓库的根目录中,创建 playbook 文件。文件名为 jenkins.yml,并应如下所示:

--- 
- hosts: "{{ target | default('localhost') }}" 
  become: yes 
  roles: 
    - jenkins 
    - nodejs 

现在我们的角色已经完成,因此我们可以提交我们新的角色并将其推送到 GitHub。按照之前描述的最佳实践,我们将从创建一个新的分支开始:

$ git checkout -b jenkins  

使用以下命令添加我们的文件:

$ git add jenkins.yml roles/jenkins  

提交并最终 push 更改:

$ git commit -m "Adding a Jenkins playbook and role" $ git push origin jenkins

从那里,在 GitHub 内部提交一个拉取请求并将分支合并回主分支:

完成后,使用以下命令返回到主分支:

$ git checkout master
$ git branch
    jenkins
  * master
$ git pull 

在实际情况中,你可能还希望定期运行以下内容:

$ git pull

这将检索其他开发人员所做的更改。

我们现在可以创建我们的 CloudFormation 模板,以便调用该角色。

创建 CloudFormation 模板

为了保持我们的代码与我们在第三章《将基础设施视为代码》中看到的代码尽可能相似,我们将从我们在该章中创建的 helloworld Troposphere 代码开始。首先,我们将复制 Python 脚本。前往你的 EffectiveDevOpsTemplates 目录,那里有你的 Troposphere 模板,然后按如下方式克隆 ansiblebase-cf-template.py 文件:

$ cp ansiblebase-cf-template.py jenkins-cf-template.py  

Jenkins 主机将需要与 AWS 进行交互。为此,我们将创建一个实例配置文件,稍后将详细描述它,利用由与 Troposphere 相同的作者开发的另一个库。我们将按如下方式安装它:

$ pip install awacs

我们现在将编辑 jenkins-cf-template.py 文件。我们将进行的前两个更改是应用程序的名称和端口。Jenkins 默认运行在 TCP/8080 上:

ApplicationName = "jenkins" 
ApplicationPort = "8080" 

我们还将设置一些与 GitHub 信息相关的常量。将 GithubAccount 的值替换为你的 GitHub 用户名或组织名:

GithubAccount = "yogeshraheja"

我们还想添加一个实例 IAM 配置文件,以更好地控制我们的 EC2 实例如何与 AWS 服务(如 EC2)进行交互。我们之前在 第二章中使用了 IAM 服务,部署您的第一个 Web 应用程序,当时我们创建了我们的用户。你可能还记得,除了创建用户外,我们还为其分配了管理员策略,这使得用户可以完全访问所有 AWS 服务。除此之外,我们还生成了访问密钥和秘密访问密钥,当前正使用它们来验证自己作为该管理员用户,并与 CloudFormation 和 EC2 等服务进行交互。

当你使用 EC2 实例时,提供的 实例配置文件 功能让你可以为实例指定一个 IAM 角色。换句话说,我们可以直接将 IAM 权限分配给 EC2 实例,而无需使用访问密钥和秘密访问密钥。

拥有一个实例配置文件将在本章后续章节中非常有用,尤其是在我们进行 CI 管道工作并将 Jenkins 实例与 AWS 托管服务集成时。为此,我们首先需要导入一些额外的库。以下内容来自 Troposphere import() 部分,请添加如下:

from troposphere.iam import ( 
    InstanceProfile, 
    PolicyType as IAMPolicy, 
    Role,  
) 

from awacs.aws import ( 
    Action, 
    Allow, 
    Policy, 
    Principal, 
    Statement, 
) 

from awacs.sts import AssumeRole 

然后,在变量 ud 实例化和实例创建之间,我们将创建并将角色资源添加到模板中,如下所示:

t.add_resource(Role(
    "Role",
    AssumeRolePolicyDocument=Policy(
        Statement=[
            Statement(
                Effect=Allow,
                Action=[AssumeRole],
                Principal=Principal("Service", ["ec2.amazonaws.com"])
            )
        ]
    )
))

和之前为角色所做的那样,我们现在可以创建实例配置文件并引用该角色。以下代码是创建角色的过程:

t.add_resource(InstanceProfile(
    "InstanceProfile",
    Path="/",
    Roles=[Ref("Role")]
))

最后,我们可以通过更新实例声明来引用我们的新实例配置文件。在 UserData=ud 后添加一个句点,并在初始化 IamInstanceProfile 的行后添加,如下所示:

t.add_resource(ec2.Instance(
    "instance",
    ImageId="ami-cfe4b2b0",
    InstanceType="t2.micro",
    SecurityGroups=[Ref("SecurityGroup")],
    KeyName=Ref("KeyPair"),
    UserData=ud,
    IamInstanceProfile=Ref("InstanceProfile"),
)

现在,文件应该如下所示:github.com/yogeshraheja/EffectiveDevOpsTemplates/blob/master/jenkins-cf-template.py。你可以保存更改,将新的脚本提交到 GitHub,并生成 CloudFormation 模板:

$ git add jenkins-cf-template.py
$ git commit -m "Adding troposphere script to generate a Jenkins instance"
$ git push
$ python jenkins-cf-template.py > jenkins-cf.template 

启动堆栈并配置 Jenkins

为了在运行 Jenkins 的 EC2 实例上创建我们的 EC2 实例,我们将按照 第三章中所做的那样,将基础设施视为代码,使用网页界面或命令行界面,如下所示:

$ aws cloudformation create-stack \
 --capabilities CAPABILITY_IAM \
      --stack-name jenkins \
      --template-body file://jenkins-cf.template \
      --parameters  
      ParameterKey=KeyPair,ParameterValue=EffectiveDevOpsAWS  

和之前一样,我们可以等待执行完成:

$ aws cloudformation wait stack-create-complete \
 --stack-name jenkins  

之后,我们可以提取主机的公网 IP:

$ aws cloudformation describe-stacks \
 --stack-name jenkins \
      --query 'Stacks[0].Outputs[0]'
    {
        "Description": "Public IP of our instance.",
        "OutputKey": "InstancePublicIp",
        "OutputValue": "18.208.183.35"
    }  

由于我们将 Ansible Jenkins 角色保持得相对简单,我们需要完成它的配置,以便完成 Jenkins 的安装。请按照以下步骤操作:

  1. 在浏览器中打开实例公有 IP 的8080端口(在我的情况下,即http://18.208.183.35:8080)。等待一段时间,直到 Jenkins 配置完成,然后你将看到屏幕:

  1. 使用以下ssh命令(请根据需要调整 IP 地址)及其远程执行命令的功能,我们可以提取管理员密码,并通过以下命令将其提供给第一个配置屏幕:

$ ssh -i ~/.ssh/EffectiveDevOpsAWS.pem ec2-user@18.208.183.35 \ 
sudo cat /var/lib/jenkins/secrets/initialAdminPassword 
  1. 在下一个屏幕上,选择安装推荐的插件。

  2. 在下一个屏幕上创建你的第一个管理员用户,并点击“保存并完成”按钮。

  3. 最后,点击“开始使用 Jenkins”按钮。

我们的 Jenkins 实例现在已经准备好使用了。

准备我们的 CI 环境

我们将使用我们的 Jenkins 实例与 GitHub 一起,使用一个合适的 CI 管道重新创建我们的helloworld应用程序。为此,我们将经过一些初步步骤,从创建一个新的 GitHub 组织开始,这个组织里有一个名为helloworld的新仓库。

创建一个新的 GitHub 组织和仓库

我们现在将创建一个新组织,并为其创建一个新的仓库,用于托管我们的helloworld节点应用程序。我们将通过以下步骤创建该组织,然后使用与第三章《将基础设施视为代码》相同的步骤,在组织内部创建一个新仓库:

  1. 在浏览器中打开github.com/organizations/new

  2. 设置组织名称,这将是你主 GitHub 账户下的一个独立 GitHub 账户。我创建的组织名称是yogeshrahejahelloworld

  3. 提供你的电子邮件 ID 并选择免费计划。

  4. 点击“创建组织”按钮,并为接下来的两个步骤选择默认设置:

  1. 为新创建的组织创建一个新仓库:

  1. 将你的仓库命名为helloworld

  2. 勾选“初始化此仓库并附带 README”复选框。

  3. 点击“创建仓库”按钮:

这将创建一个仓库、一个主分支,以及一个README.md文件。

一个合适的 CI 管道在后台静默运行。为了实现这一点,当代码托管在 GitHub 上时,Jenkins 需要从 GitHub 获取通知,以表明代码已发生变化,从而触发自动构建。这是我们可以通过一个叫做github-organization-plugin的插件轻松实现的。这个插件是我们在选择安装推荐插件时,Jenkins 中安装的插件之一。为了使用它,我们首先需要在 GitHub 中创建一个个人访问令牌。

创建一个 GitHub 个人访问令牌

创建个人访问令牌将使插件能够访问推送到 GitHub 的代码,并创建必要的钩子,以便在发生新的提交和拉取请求时获取通知。为了创建令牌,请按照以下步骤操作:

  1. 在浏览器中打开 github.com/settings/tokens

  2. 点击生成新令牌按钮。

  3. 给它起一个描述性的名称,例如 Effective DevOps with AWS Jenkins

  4. 选择 repoadmin:repo_hookadmin:org_hook 范围。

  5. 点击生成令牌按钮。

  6. 这将带你回到主令牌页面。保存生成的令牌,我们稍后将需要它。

将访问令牌添加到 Jenkins 中的凭证

现在,我们可以按照以下步骤将令牌添加到 Jenkins 中:

  1. 打开 Jenkins,在我的案例中是 http://18.208.183.35:8080

  2. 在左侧菜单中点击凭证,然后点击系统,接着点击全局凭证。

  3. 在下一个屏幕上,点击添加凭证。

  4. 我们将要创建的凭证类型是用户名与密码。

  5. 范围应该是全局的。

  6. 使用你的 GitHub 组织作为用户名。

  7. 使用上一部分生成的令牌作为密码。

  8. ID 可以是类似 GitHub 的名称,如下截图所示:

  1. 你也可以选择为它提供一个描述。之后点击 OK。

我们初始化过程的最后一步是创建 Jenkins 作业。

创建 Jenkins 作业以自动运行构建

如前所述,Jenkins 有一个插件来帮助与 GitHub 集成。我们可以通过创建一个 GitHub 组织作业轻松利用这个插件。按照以下步骤操作:

  1. 在浏览器中打开 Jenkins 首页,输入 http://18.208.183.35:8080/ 并点击创建新作业。

  2. 输入项目名称,提供你的 GitHub 用户名或组织名称,点击 GitHub 组织,然后点击 OK。

  3. 这将带我们到一个新页面,我们将在这里配置项目:

    1. 在凭证下拉菜单中,选择你新创建的凭证。

    2. 验证所有者是你的用户名、组织名称,或创建作业时提供的名称。这将被 Jenkins 用来扫描你所有的代码库。

    3. 由于我们已经知道我们只关心 helloworld 仓库,点击“行为”部分底部的“添加”按钮,然后选择第一个选项,即“按名称过滤(使用正则表达式)”。

    1. 在新出现的字段中,常规表达式,将.*替换为helloworld。在“发现分支”部分选择策略为“所有分支”,然后向下滚动,在同一页面的“扫描组织触发器”部分选择一分钟:

      1. 点击保存。

该工作将被创建并扫描项目以找到一个分支。它会找到包含README文件的 master 分支,但因为我们还没有代码,所以我们不会做任何事情。在接下来的部分,我们将解决代码缺失的问题并实现我们的helloworld应用程序:

使用我们的 CI 环境实现 helloworld 应用程序

在这里,我们将再次使用我们在第二章中创建的简单helloworld Web 应用程序,部署您的第一个 Web 应用程序。这里的目标更多是为了说明我们 CI 管道的使用,而不是构建一个复杂的 Web 应用程序:

初始化项目

我们将使用在上一节中为 Jenkins 部署和配置的相同 AWS 实例作为开发环境。因此,我们需要在实例上安装nodejsnpm。如果您还没有安装它们,请参考第二章中的说明,部署您的第一个 Web 应用程序:

$ ssh -i ~/.ssh/EffectiveDevOpsAWS.pem ec2-user@18.208.183.35
$ node –v
$ npm –v 

运行前述命令的输出如下:

我们的第一步将是克隆我们在上一节中创建的helloworld GitHub 仓库:

$ git clone https://github.com/<your_github_organization>/helloworld.git
$ cd helloworld

我们现在可以创建一个新的分支:

$ git checkout -b initial-branch  

创建一个空文件,名为helloworld.js

$ touch helloworld.js  

为这些类型的项目编写测试的最佳方法之一是使用测试驱动开发TDD)方法。在 TDD 过程中,开发人员首先创建测试,然后运行它们以确保它们失败,编写代码,再次进行测试。此时,测试应通过。我们可以创建一个拉取请求并在审查和批准后进行合并。

使用 Mocha 创建功能测试

为了说明我们 TDD 方法中编写测试的过程,我们将使用一个名为Mocha的工具(mochajs.org/)。Mocha 是一个非常常见且易于使用的 JavaScript 测试框架,用于创建测试。

我们将使用以下npm,即 Node.js 包管理器命令,在本地安装它到我们的系统中。

首先,我们将使用以下命令初始化npm

$ npm config set registry http://registry.npmjs.org/
$ npm init –yes

运行前述命令的输出如下:

这将创建一个名为package.json的新文件。接下来,我们将安装 Mocha 并将其添加到我们的开发依赖列表中,如下所示:

$ npm install mocha@2.5.3 --save-dev

这将创建一个名为node_modules的目录,Mocha 将被安装在该目录中。

除了 Mocha,我们还将使用一个无头浏览器测试模块来渲染我们的helloworld应用程序,名为Zombie。我们可以通过以下相同命令安装它:

$ npm install zombie@3.0.15 --save-dev 

为了将测试与项目的其余部分分开,我们现在将在helloworld项目的根目录下创建一个名为test的目录。默认情况下,Mocha 会在该目录中查找测试:

$ mkdir test 

我们将使用的最后一段样板代码将配置npm,使其使用 Mocha 来运行我们的测试。使用您的编辑器,打开package.json文件,并将测试脚本替换为以下命令:

 "scripts": {
 "test": "node_modules/mocha/bin/mocha"
 },

test目录内,创建并编辑文件helloworld_test.js

第一步是加载我们将在测试中使用并需要的两个模块。第一个是zombie,我们的无头浏览器,第二个是assert模块,它是用于在 Node.js 应用程序中创建单元测试的标准模块:

var Browser = require('zombie') 
var assert = require('assert') 

接下来,我们需要加载我们的应用程序。这可以通过调用相同的require()函数来完成,但这次我们将让它加载我们即将实现的helloworld.js文件。目前,它是一个空文件:

var app = require('../helloworld')

现在我们可以开始创建测试了。Mocha 的基本语法试图模仿它认为规范文档可能需要的内容。以下是三个必需的语句,请添加以下内容:

describe('main page', function() { 
  it('should say hello world')
})

我们现在需要向测试中添加钩子,以便与我们的 web 应用程序进行交互。

第一步是将测试指向我们的应用程序端点。如您可能记得,从前面的章节中,应用程序正在http://localhost:3000上运行。我们将使用名为before()的钩子来设置一个前提条件。在it()调用之上,添加以下内容,将我们的无头浏览器指向正确的服务器:

describe('main page', function() {
before(function() {
 this.browser = new Browser({ site: 'http://localhost:3000' })
})

it('should say hello world')
}) 
...

此时,我们的无头浏览器将连接到我们的应用程序,但它不会请求任何页面。让我们在另一个before()钩子中添加这一点,如下所示:

describe('main page', function() { 
  before(function() { 
    this.browser = new Browser({ site: 'http://localhost:3000' }) 
  })

 before(function(done) { this.browser.visit('/', done) })

  it('should say hello world') 
})
...

现在首页已经加载,我们需要在it()函数中实现代码来验证我们的断言。我们将编辑包含it()调用的行,并添加一个回调函数,如下所示:

describe('main page', function() { 
  before(function() {
    this.browser = new Browser({ site: 'http://localhost:3000' })
  })
  before(function(done) {
    this.browser.visit('/', done)
  })
  it('should say hello world', function() { 
    assert.ok(this.browser.success)
    assert.equal(this.browser.text(), "Hello World")
  })
})

我们的测试现在已经准备好。如果一切顺利,您的代码应该与以下链接中所示的代码相似:raw.githubusercontent.com/yogeshraheja/helloworld/master/test/helloworld_test.js

我们可以通过在终端中简单地调用 Mocha 命令来进行测试,如下所示:

$ npm test

./node_modules/mocha/bin/mocha
 main page
 1) "before all" hook
  0 passing (48ms)
  1 failing
  1) main page "before all" hook:
 TypeError: connect ECONNREFUSED 127.0.0.1:3000  

如您所见,我们的测试失败了。它无法连接到 web 应用程序。这当然是预期的,因为我们还没有实现应用程序代码。

开发应用程序的其余部分

我们现在准备好开发我们的应用程序了。由于我们已经在第二章《部署你的第一个 Web 应用程序》中完成了相同代码的创建,我们只需按如下方式复制或直接下载它:

$ curl -L https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.js > helloworld.js

我们现在可以使用npm命令重新测试代码:

$ npm test
Server running
 main page
      should say hello world
  1 passing (78ms)  

执行前述命令的输出如下:

我们的测试现在已经通过。

我们快完成了。我们已经完成了第一个目标,即为我们的代码提供测试覆盖率。当然,一个更复杂的实际应用会有更多的测试,但现在我们要关注的是自动化。既然我们已经学会了如何手动测试代码,我们想看看 Jenkins 如何为我们完成这一工作。

在 Jenkins 中创建 CI 管道

正如我们之前看到的,Jenkins 通过创建和执行作业来工作。历史上,创建管道的一种方式是打开浏览器中的 Jenkins,导航到我们之前创建的作业,并编辑它以概述测试代码的不同步骤。这个解决方案的问题在于,它没有一个好的审核过程,很难跟踪每次更改。此外,对于开发者来说,在一个涉及添加新构建步骤的项目中进行更改是非常困难的,因为项目的代码和构建该项目的作业没有同步在一起。Jenkins 2 将描述构建过程为本地文件的概念作为标准功能,我们将在接下来的部分中使用它。

我们将创建并编辑项目中的新文件 Jenkinsfile(大写的 J,没有文件扩展名)。该文件将使用 Groovy 编写(www.groovy-lang.org)。

在文件的第一行,我们将放置以下内容:

#!groovy 

这对于不同的 IDE 和 GitHub 来说是有用的,因为它标明了文件的性质。我们脚本的第一步将是要求 Jenkins 将任务分配给节点,如下所示:

node { } 

我们的 Jenkins 安装相当简单。我们只有一台服务器,因此只有一个节点。如果我们有更多的节点,我们可以向调用中添加参数,以便将任务指向具有特定架构的节点,甚至可以实现并行执行。

我们的 CI 测试可以逻辑上分为几个步骤:

  1. 从 GitHub 获取代码。

  2. 通过调用 npm install 命令来安装不同的依赖项。

  3. 使用命令 mocha 运行我们的测试。

  4. 清理。

这些步骤在 Jenkins 中有一个等价的概念,叫做 stages(阶段)。我们将把它们添加到节点路由中。以下是第一个阶段的样子:

node { 
   stage 'Checkout' 
        checkout scm 
} 

这告诉 Jenkins 从源代码管理获取代码。当我们创建作业时,我们声明它是一个 GitHub 组织作业,因此 Jenkins 会知道如何正确地解释这一点。

接下来,我们需要调用 npm install 命令。Groovy 无法理解诸如调用 npm 这样的特定语言功能。为此,我们将使用 sh 命令,它允许我们启动一个 shell 并执行命令。我们的第二个阶段如下所示:

stage 'Checkout'
    checkout scm

stage 'Setup'
    sh 'npm config set registry http://registry.npmjs.org/'
    sh 'npm install'

在接下来的阶段中,我们将运行 Mocha。以下是 Setup 阶段;请添加以下内容:

   stage 'Mocha test' 
        sh './node_modules/mocha/bin/mocha' 

最后,我们可以继续清理代码库,使用以下阶段:

stage 'Cleanup'
        echo 'prune and cleanup'
        sh 'npm prune'
        sh 'rm node_modules -rf'

Jenkins 文件现在已经准备好,它应如下所示:raw.githubusercontent.com/yogeshraheja/helloworld/master/Jenkinsfile

我们现在可以提交我们的代码并进行测试:

$ git add Jenkinsfile helloworld.js package.json test
$ git commit -m "Helloworld application"
$ git push origin initial-branch

这将创建一个名为initial-branch的远程分支。当分支创建时,Jenkins 会收到来自 GitHub 的通知,并会运行 CI 流水线。在几秒钟内,我们的测试将在 Jenkins 上运行,Jenkins 又会将结果返回给 GitHub。我们可以通过以下方式观察这一过程:

  1. 在浏览器中打开 GitHub,导航到你创建的helloworld项目。

  2. 点击 Branch 并选择 initial-branch。

  3. 在该页面上,点击 New pull request,提供一个标题并简要描述你正在进行的更改。如果可能,提到其他开发人员,以便他们可以彻底审查你提出的更改。

  4. 点击 Create pull request 并按照步骤创建拉取请求。一旦拉取请求创建完成,你将能够看到 GitHub 如何显示该拉取请求已通过所有检查:

  1. 你还可以进入 Jenkins 浏览器并查看构建历史。你甚至可以通过点击组织,接着是仓库和分支,查看 Jenkins 的详细信息。这将把我们带回 Jenkins 作业页面,在那里你可以更详细地观察作业和流水线的执行:

  1. 此时,如果你提到其他开发人员,他们应该会收到通知,这样他们可以查看拉取请求的内容。一旦审核通过,拉取请求可以合并。从那时起,当开发人员拉取 master 分支或进行 rebase 操作时,他们将能看到你的代码。

根据团队在仓库中的规模,通常需要对分支进行 rebase。最重要的两个时机是在创建拉取请求之前(步骤 2)和合并之前(步骤 6)。

将 CI 流水线投入生产

我们现在已经建立了一个基本的、但功能完备的 CI 流水线。虽然这是一个不错的起点,但你可能还希望完善这个系统的某些细节。如前所述,我们为 Jenkins 配置的 Ansible 配方可以改进,以包含诸如我们手动创建的helloworld作业的配置。

我们只创建了一个功能性测试来演示如何使用 TDD 方法以及如何将测试步骤集成到我们的流水线中。持续集成流水线的成功在很大程度上依赖于所编写的测试的质量和数量。测试通常会分为功能性测试和非功能性测试。为了最好地利用你的流水线,你需要尽早捕捉到可能的错误。这意味着要集中精力做功能性测试,特别是单元测试,这些测试用于验证小单元的代码,例如类中的方法。

之后,你可以专注于集成测试,它涵盖的范围更广,通常涉及数据存储和代码中的其他功能。最后,你还需要添加验收测试,以验证你的故事需求是否完整:

在非功能性测试方面,你通常会关注性能安全性可用性兼容性测试。

最后,你可以借助代码分析工具来补充自己的测试,以了解代码覆盖率(自动化测试执行了多少行代码)。

一如既往,DevOps 中收集指标非常重要。在持续集成(CI)流水线中,你通常需要监控通过 CI 流水线的构建数量以及拉取请求的质量。

像任何其他系统一样,你需要花一些时间来设置备份和监控。如果你还没有迁移到由配置管理系统(如 Ansible)管理你的作业和 Jenkins 配置的模型,你可能会决定备份 Jenkins 主目录。在指标方面,监控系统性能、可用性和健康状况是至关重要的。构建流水线出现故障应该被视为一个关键问题,因为它会影响所有开发人员和运维人员的生产力。

最后,你应该预期随着时间推移,CI 基础设施需要扩展。随着代码和测试的增加,运行测试的时间会越来越长。你可能会决定增加更多的 Jenkins 从机,这样可以并行运行测试和/或使用更强大的实例。在当前格式下,每次有变更推送到分支时,Jenkins 都会运行 helloworld 流水线。你还可以决定仅在创建拉取请求时才运行流水线。

在本章的初始部分,我们采用了一个新工作流,开发人员将代码和测试提交到各自的分支,并频繁发送拉取请求,与其他工程师共享提议的变更。此外,我们通过创建持续集成流水线确保新代码经过充分测试。为此,我们创建了一个 Jenkins 服务器并将其与 GitHub 连接。通过该系统,所有与项目一起提交的测试会自动执行,测试结果会返回到 GitHub。现在,我们处于一个理想的状态,可以将工作流提升到下一个层次,并实现部署自动化。

DevOps 是否不再需要 QA 团队?

是的,也不是。在一个高效的 DevOps 组织中,通常不需要非技术性的 QA 工作。如果一切都已完全自动化,并且开发人员编写了足够的测试覆盖所有代码的方面,那么组织不需要安排人来编写和执行测试计划。相反,专注于 DevOps 的组织将有工程师,有时被称为 QA 工程师,他们专注于质量,但重点是自动化。这涉及到开发工具和流程,以提高自动化测试代码的能力。

构建持续部署流水线

通过创建 CI 流水线,我们迈出了成为高效工程组织的第一步。因为我们的工作流现在包括在各个分支上进行工作,并在经过自动化测试和人工审核后将其合并回主分支,我们可以假设主分支中的代码质量较高,并且可以安全部署。现在我们可以专注于下一个挑战,那就是在新代码合并到主分支后,自动发布代码。

通过持续发布新代码,你大幅加快了 DevOps 提供的反馈循环过程。以高速将新代码发布到生产环境,能够收集真实的客户数据,这通常会暴露出新的且往往是意想不到的问题。对于许多公司来说,将新代码部署到生产环境是一项挑战。特别是当涉及到成千上万的新的提交同时发布到生产环境时,这种过程一年只有几次,可能会让人感到焦虑。做这件事的公司通常会安排在深夜或周末进行维护。采用更现代化的方法,像我们将在后续章节中介绍的,将对工程团队的工作与生活平衡产生显著的积极影响。

像 Google 或 Facebook 这样的知名科技公司通常不在周五部署代码。目的是避免在周末前发布存在漏洞的代码,这可能导致周六或周日出现意外问题。因为他们不害怕部署代码,许多变更会在高峰时段发布到生产环境,以便能够快速捕捉到与负载相关的问题。

为了实现我们的持续部署流水线,我们将重点介绍两个新的 AWS 服务——CodePipelineCodeDeploy

  • CodePipeline 让我们可以创建部署流水线。我们将告诉它从 GitHub 获取代码,像之前一样,将其发送到 Jenkins 进行 CI 测试。然而,与其仅仅将结果返回到 GitHub,我们接着会通过 AWS CodeDeploy 将代码部署到我们的 EC2 实例上。

  • CodeDeploy 是一项服务,允许我们将代码正确地部署到 EC2 实例上。通过添加一定数量的配置文件和脚本,我们可以使用 CodeDeploy 来可靠地部署和测试我们的代码。得益于 CodeDeploy,我们不必担心部署顺序的复杂逻辑。它与 EC2 紧密集成,知道如何在多个实例之间执行滚动更新,并在需要时执行回滚。

在第三章《将基础设施视为代码》中,我们查看了如何使用 Ansible 配置服务器并部署helloworld应用程序。虽然这个解决方案允许我们演示如何使用配置管理,但它对于更关键的服务来说并不够好。没有顺序概念,也没有良好的反馈机制来告诉我们部署是否成功,此外我们也没有实现任何验证步骤。

拥有一个专门针对 AWS 部署的服务,将使得部署应用程序变得更加高效,正如我们将在接下来的部分中看到的那样。为了演示这些服务,我们将首先使用 Ansible 构建一个新的通用 Node.js web 服务器。

为持续部署创建新的 web 服务器

为了使用 CodeDeploy,EC2 实例需要运行 CodeDeploy 代理。通常通过从 S3 存储桶中下载一个可执行文件来完成此操作,具体取决于你的实例所在的区域。方便的是,AWS 还发布了一个自定义 Ansible 库,可以自动化这些步骤。由于该库不是标准的 Ansible 库的一部分,我们首先需要将其添加到我们的 Ansible 仓库中。

将自定义库导入 Ansible 以用于 AWS CodeDeploy

默认情况下,Ansible 期望在/usr/share/my_modules/目录中找到自定义库。之前,我们在第三章《将基础设施视为代码》中查看了清单脚本,将基础设施视为代码,并通过编辑ansible.cfg文件更改了这个默认行为。我们将进行必要的更改,使得库文件与其他 Ansible 文件一起下载到主机上。完成此操作的最简单方法是在ansible仓库的根目录创建一个新目录,并将库放在其中。

在你的电脑上,打开终端并进入你的ansible目录:

在我们ansible仓库的根目录中,ansible.cfg文件所在的位置,我们将添加一个新的目录库,用于存放 AWS CodeDeploy ansible库:

$ mkdir library  

文件夹创建完成后,我们可以将ansible库下载到其中:

$ curl -L https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter05/ansible/library/aws_codedeploy > library/aws_codedeploy

最后,我们将编辑位于ansible仓库根目录中的ansible.cfg文件,以指定库文件夹的位置,如下所示:

# update ansible.cfg 
[defaults]
inventory = ./ec2.py 
remote_user = ec2-user 
become = True 
become_method = sudo 
become_user = root 
nocows = 1
library = library

我们现在准备开始使用这个库。CodeDeploy 是一个我们可能会随着新服务加入系统而不断重用的服务。为了确保我们的 Ansible 仓库代码符合不要重复自己DRY)原则,我们将创建一个专门用于 CodeDeploy 的 Ansible 角色。

创建一个 CodeDeploy Ansible 角色

我们将首先进入位于ansible仓库根目录的角色目录:

$ cd roles  

如前所述,我们将依赖ansible-galaxy来设置创建角色所需的框架:

$ ansible-galaxy init codedeploy  

我们的角色将非常简单。我们将编辑codedeploy/tasks/main.yml文件,并调用aws_codedeploy库提供的新模块,如下所示:

---
# tasks file for codedeploy
- name: Installs and starts the AWS CodeDeploy Agent
 aws_codedeploy: 
    enabled: yes  

此时,我们可以为通用nodejs Web 服务器创建新的剧本。首先,返回到ansible仓库的根目录:

$ cd ..  

创建一个名为nodeserver.yml的新文件:

$ touch nodeserver.yml  

我们将采用与之前其他剧本相同的方法。我们的服务器目标是运行 Node.js 应用程序并启动 CodeDeploy 守护进程。编辑nodeserver.yml文件,并添加如下内容:

--- 
- hosts: "{{ target | default('localhost') }}" 
  become: yes 
  roles: 
    - nodejs 
    - codedeploy 

在使用 CodeDeploy 与配置管理系统(如 Ansible 或 CloudFormation)时,重要的是始终在启动应用程序之前安装所有依赖项。这可以帮助您避免竞态条件。

现在我们可以将更改提交到git。首先,创建一个新分支,然后添加我们创建的新文件和目录:

$ git checkout -b code-deploy $ git add library roles/codedeploy nodeserver.yml ansible.cfg

最后,commitpush更改:

$ git commit -m "adding aws_codedeploy library, role and a nodeserver playbook"
$ git push origin code-deploy

如前所述,您现在可以创建一个拉取请求。一旦拉取请求被审查和批准,将其合并回主分支。按照这些步骤操作后,您的 Ansible 仓库应该如下所示:github.com/yogeshraheja/Effective-DevOps-with-AWS/tree/master/Chapter05/ansible

创建 Web 服务器 CloudFormation 模板

现在我们已经准备好了 Ansible 剧本,可以使用 Troposphere 创建 CloudFormation 模板。首先,复制我们之前为 Jenkins 创建的 Troposphere 脚本:

$ cd EffectiveDevOpsTemplates
$ cp jenkins-cf-template.py nodeserver-cf-template.py

编辑nodeserver-cf-template.py文件,进行如下更改。首先,通过更新变量来更改应用程序名称和端口:

ApplicationName = "nodeserver" 
ApplicationPort = "3000" 

此外,我们的实例需要从 S3 下载文件。为了实现这一点,替换允许 Jenkins 实例上 CodePipeline 的策略,使用允许 S3 的策略。编辑名为AllowCodePipeline的策略,更新其名称和操作。在实例化我们的实例上方,添加一个新的 IAM 策略资源,如下所示:

t.add_resource(IAMPolicy( 
    "Policy", 
    PolicyName="AllowS3", 
    PolicyDocument=Policy( 
        Statement=[ 
            Statement( 
                Effect=Allow, 
                Action=[Action("s3", "*")], 
                Resource=["*"]) 
        ] 
    ), 
    Roles=[Ref("Role")] 
)) 

新的脚本应该如下所示:raw.githubusercontent.com/yogeshraheja/EffectiveDevOpsTemplates/master/nodeserver-cf-template.py

由于新脚本已经准备好,我们可以保存它并按以下方式生成 CloudFormation 模板:

$ git add nodeserver-cf-template.py
$ git commit -m "Adding node server troposhere script"
$ git push
$ python nodeserver-cf-template.py > nodeserver-cf.template

启动我们的 Web 服务器

和以前一样,我们将使用 CloudFormation 启动我们的实例。请注意,我们将第一个堆栈命名为 helloworld-staging。我们将首先将 CodeDeploy 用作将代码部署到暂存环境的一种方式。我们将在 CodeDeploy 中使用这个名称,以便将部署目标定位到该特定堆栈:

 $ aws cloudformation create-stack \
    --capabilities CAPABILITY_IAM \
    --stack-name helloworld-staging \
    --template-body file://nodeserver-cf.template \
    --parameters ParameterKey=KeyPair,ParameterValue=EffectiveDevOpsAWS  

几分钟后,我们的实例将准备好。

我们现在正处于 DevOps 转型的一个重要节点。我们已经创建了通用的 nodejs Web 服务器,允许你轻松地在其上部署代码。我们距离一个现实的环境非常接近,企业通常用来部署和运行其服务。我们能够轻松、按需创建这些环境,正是我们成功的关键。

在架构服务时,始终确保基础设施可以轻松重建。能够排查问题是很好的,但能够快速重建服务主机并停止对用户的影响,通常是更为理想的。

将我们的 helloworld 应用程序与 CodeDeploy 集成

现在我们的服务器已经初始化,CodeDeploy 代理正在运行,我们可以开始使用它们了。首先,我们需要为 CodeDeploy 创建一个 IAM 服务角色。接着,我们需要在 CodeDeploy 服务中添加一个条目来定义我们的应用程序。最后,我们需要添加我们的应用程序规范文件以及一些脚本,帮助部署和运行我们的服务到 helloworld 应用程序。

创建 CodeDeploy 的 IAM 服务角色

CodeDeploy 权限在 IAM 中是以单个应用程序为单位工作的。为了提供足够的权限,我们将创建一个新的 IAM 服务角色,并附加以下策略:

{ 
  "Version": "2012-10-17", 
  "Statement": [ 
    { 
      "Sid": "", 
      "Effect": "Allow", 
      "Principal": { 
        "Service": [ 
          "codedeploy.amazonaws.com" 
        ] 
      }, 
      "Action": "sts:AssumeRole" 
    } 
  ] 
} 

我们将使用以下命令在命令行界面中创建一个名为 CodeDeployServiceRole 的新角色:

$ aws iam create-role \
 --role-name CodeDeployServiceRole \
 --assume-role-policy-document \
 https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-    
    with-AWS/master/Chapter05/misc/CodeDeploy-Trust.json

我们现在需要附加角色策略,以为服务角色提供适当的权限:


$ aws iam attach-role-policy \
 --role-name CodeDeployServiceRole \
 --policy-arn \
 arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole 

我们的 IAM 服务角色现在已经准备好。我们终于可以开始与 CodeDeploy Web 界面交互了。

创建 CodeDeploy 应用程序

现在,我们已经启动了 EC2 实例,并在其上运行 CodeDeploy 服务,定义了我们的 IAM 服务角色,我们具备了创建 CodeDeploy 应用程序所需的所有条件。像往常一样,AWS 服务有许多使用方式,但在这一部分,我们将通过 Web 界面演示其基本用法:

  1. 在浏览器中打开 console.aws.amazon.com/codedeploy

  2. 如果有提示,点击立即开始。

  3. 这将带我们进入一个欢迎页面,提供两个选项,Sample Deployment 和 Custom Deployment。选择 Custom Deployment 并点击跳过操作指南。接着,我们将进入一个名为创建应用程序的表单。

  4. 在表单中,在应用程序名称下,给我们的应用程序命名为 helloworld

  5. 部署组可以被视为应用程序将要运行的环境。我们将首先创建一个暂存环境。在“Deployment Group Name”字段中,提供名称staging

  6. 现在我们需要向我们的应用程序添加实例。我们的目标是针对之前使用 CloudFormation 创建的 EC2 实例。如你所记,我们把我们的堆栈命名为helloworld-staging。在“环境配置”部分,选择 Amazon EC2 实例,在“Key”字段选择aws:cloudformation:stack-name,在“Value”字段选择helloworld-staging。这将确保 CodeDeploy 只选择我们打算用于应用程序的实例。AWS CodeDeploy 应该确认它匹配到了一个实例:

  1. 接下来的部分叫做部署配置。CodeDeploy 的一个优势是它能够理解如何将代码部署到服务器集群。这一功能使得在部署过程中能够轻松避免停机。默认情况下,服务提供三种部署选项——一次一个、一次全部、和一半一个。虽然可以创建自定义的部署配置,但在我们的案例中,由于我们只有一个实例,可以保留默认选项CodeDeployDefault.OneAtATime

  2. 接下来的两个部分被称为触发器和警报。在本书中我们不会详细讲解这些内容,但基本上,触发器在收集部署和监控相关的指标时非常有用。通过创建触发器,将通知推送到 SNS 并创建 CloudWatch 指标,你可以轻松收集与部署相关的指标。这有助于你回答诸如发生了多少次部署、多少次失败、多少次部署导致回滚等问题。

  3. 由于我们的应用程序是无状态的,因此在失败时启用回滚是个好主意。选择“部署失败时回滚”选项。

  4. 最后,我们需要选择在之前步骤中创建的服务角色。在“Service Role ARN”字段中,选择以 CodeDeployServiceRole 结尾的角色。

  5. 最后,点击“创建应用程序”。

这将带我们回到 CodeDeploy 应用页面,展示我们新创建的helloworld应用。

在 CodeDeploy 中创建应用程序使我们能够定义我们新创建的应用程序将被部署到哪里。现在我们来看看如何部署我们的代码。

将 CodeDeploy 配置和脚本添加到我们的仓库

当我们在本章早些时候创建 Jenkins 流水线时,我们在helloworld GitHub 仓库中创建了一个 Jenkinsfile 文件。这样做的原因是我们可以在同一更改集中同时更改代码和代码的测试方式。出于同样的原因,将如何部署代码的逻辑与代码本身放在一起是个好主意。

我们的helloworld仓库目前包含我们在一个新的 GitHub 组织(在我的情况下是yogeshrahejahelloworld)中创建的应用程序。它还包含应用程序的测试和一个名为helloworld的仓库。我们现在将添加 CodeDeploy 所需的信息,以便执行我们的服务部署。

CodeDeploy 依赖于名为appspec.yml的应用程序规范文件来管理部署。我们首先需要创建这个文件。进入克隆了helloworld GitHub 项目的目录并从 master 分支创建一个新分支:

$ git clone https://github.com/<YOUR GITHUB ORGANIZATION>/helloworld.git
$ cd helloworld
$ git checkout -b helloworld-codedeploy 

现在我们将创建并编辑文件appspec.yml

$ touch appspec.yml  

在文件的第一行,我们将定义想要使用的 AppSpec 文件版本。目前,唯一支持的版本是0.0

version: 0.0 

在下一行,我们将指定我们希望部署服务的操作系统。在我们的案例中,这是 Linux:

os: linux 

现在我们将描述每个文件应该放在哪里。为此,我们将创建一个名为files的部分,并使用格式source destination将我们要部署的每个文件放入其中。请注意,文件是用 YAML 编写的,因此空格和对齐非常重要:

version: 0.0 
os: linux 
files:
 - source: helloworld.js
 destination: /usr/local/helloworld/

通过这一部分,CodeDeploy 现在知道将helloworld.js复制到目标位置/usr/local/helloworld。我们的helloworld目录将由 CodeDeploy 自动创建。为了启动应用程序,我们还需要我们的 upstart 脚本,而它目前不在仓库中。

回到helloworld项目的根目录的终端,我们将创建一个名为scripts的子目录,并将 upstart 脚本添加到其中:

$ mkdir scripts
$ wget https://raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter02/helloworld.conf -O scripts/helloworld.conf

我们现在可以将新的helloworld.conf文件添加到我们的appspec.yml中,通过添加另一个块来指定 upstart 脚本的源和目标位置,如下所示:

files:
  - source: helloworld.js
    destination: /usr/local/helloworld/
  - source: scripts/helloworld.conf 
    destination: /etc/init/

运行我们的应用程序作为服务所需的两个文件现在已经放置在适当的位置。为了部署我们的应用程序,我们还需要更多的文件。我们需要 CodeDeploy 来启动和停止服务。之前,我们使用 Ansible 来启动应用程序,但这次我们不再使用 Ansible 来管理服务。CodeDeploy 提供了一个更优雅的解决方案:当部署开始时,运行在 EC2 实例上的 CodeDeploy 代理将按照以下事件序列进行操作:

包含我们应用程序的档案将在DownloadBundle事件期间下载到系统中。安装部分将用于将我们模板中定义的文件复制到它们的目标位置。

CodeDeploy 使用钩子(hooks)的概念。在appspec.yml文件中,我们可以创建多个钩子,在之前描述的每个阶段执行自定义脚本。我们将创建三个脚本:一个脚本用来启动应用程序,一个脚本用来停止它,最后一个脚本用来检查部署是否成功。

我们将这三个脚本放在之前创建的scripts目录中。让我们创建第一个文件start.sh并开始编辑它:

$ touch scripts/start.sh  

该脚本非常简单。我们只是简单地调用 upstart 来启动服务:

#!/bin/sh
start helloworld  

这就是我们所需要的一切。现在我们要创建我们的停止脚本文件:

$ touch scripts/stop.sh  

正如以前所做的那样,按以下方式编辑它:

#!/bin/sh
[[ -e /etc/init/helloworld.conf ]] \ 
   && status helloworld | \
      grep -q '^helloworld start/running, process' \ 
   && [[ $? -eq 0 ]] \
   && stop helloworld || echo "Application not started"

停止脚本比启动脚本稍微复杂,因为它将在BeforeInstall步骤期间执行。基本逻辑相同:我们正在调用停止helloworld应用程序。在此之前我们有一些额外的调用,因为我们需要处理第一次部署的情况,即应用程序在安装和启动之前尚未安装。

我们将创建的最后一个脚本称为validate.sh

$ touch scripts/validate.sh  

再次,代码非常简单:

#!/bin/sh
curl -I localhost:3000  

对于本书的目的,我们正在进行尽可能基本的验证。这包括对我们的应用程序唯一路由的 HEAD 请求。在更真实的应用程序中,我们将测试更多的路由和在推送新代码时可能出错的任何内容。

我们的脚本需要是可执行的,以避免 CodeDeploy 中的任何不必要的警告:

$ chmod a+x scripts/{start,stop,validate}.sh 

现在我们可以在我们的appspec.yml文件中添加我们的钩子。重新打开文件并在files部分下创建一个hooks部分:

version: 0.0 
os: linux 
files: 
[...] 
hooks: 

我们将首先声明我们希望在BeforeInstall阶段运行的停止脚本。在 hooks 部分中,添加以下内容:

hooks: 
  BeforeInstall: 
    - location: scripts/stop.sh 
      timeout: 30 

我们允许30秒来完成停止命令的执行。我们将重复类似的操作以添加我们的启动和验证脚本如下:

hooks: 
  BeforeInstall: 
    - location: scripts/stop.sh 
      timeout: 30 
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 30
  ValidateService:
    - location: scripts/validate.sh

当我们的部署管道运行时,它将尝试执行以下操作:

  1. 下载我们的应用程序包并在临时目录中解压缩它

  2. 运行停止脚本

  3. 复制应用程序和 upstart 脚本

  4. 运行启动脚本

  5. 运行验证脚本以确保一切正常运行

我们可以将所有新文件添加到git中,提交并推送更改,并按以下方式发送拉取请求:

$ git add scripts appspec.yml
$ git commit -m "Adding CodeDeploy support to the application"
$ git push

分支将通过 Jenkins 进行测试。同行可以审查代码更改;一旦批准,您可以合并您的拉取请求。

为了进行部署,我们基本上需要回答三个问题—我们试图部署什么? 我们试图将其部署到哪里? 我们如何部署它? 在我们创建了 CodeDeploy 作业时,我们已经回答了第二个问题,并且在我们的 appspec 文件及其辅助脚本中回答了第三个问题。现在我们需要研究第一个问题—我们试图部署什么? 这就是我们将使用 AWS CodePipeline 的地方。

使用 AWS CodePipeline 构建我们的部署管道

AWS CodePipeline 是一项专门用于创建交付流水线的服务。你可以把它看作是类似于 Jenkins 流水线功能的 AWS 版本。这项服务与 AWS 生态系统的其他部分高度集成,这意味着它相较于 Jenkins 具有许多优秀的功能和优势。由于它是完全托管的服务,你不必像使用单个 Jenkins 实例时那样担心其正常运行时间。它与 CodeDeploy 即开即用集成,这对于我们的情况非常方便。虽然我们在这里不详细讨论,但该服务完全与 IAM 服务集成,这意味着你可以非常精细地控制谁可以做什么。例如,该服务可以阻止未经授权的用户执行部署。凭借其 API,多个服务可以集成到你的流水线中,包括 Jenkins 和 GitHub。

我们将首先探讨如何创建一个包含两个阶段的基本流水线。在第一阶段,我们将从 GitHub 获取代码,将其打包,并将包存储在 S3 中。在第二阶段,我们将使用 CodeDeploy 将该包部署到我们的暂存实例中。

接下来,我们将通过一个更高级的场景。我们将看看如何利用我们的 Jenkins 实例在将代码部署到暂存环境之前运行测试。我们还将创建一个生产环境,并添加一个按需的生产部署过程,称为持续交付流水线。最后,我们将探讨几种策略,帮助我们建立对通过流水线推送代码的信心,以便最终能够移除按需生产部署步骤,将其转变为完全自动化的流水线。

为暂存环境创建持续部署流水线

要创建我们的第一个部署流水线,使用 CodePipeline,我们将利用 AWS 控制台,这提供了一个非常直观的 Web 界面:

  1. 在浏览器中打开以下链接: https😕/console.aws.amazon.com/cod epipeline

  2. 如果提示,点击“开始使用”。

  3. 在下一个页面,给你的管道命名为helloworld,然后点击“下一步”。

  4. 对于源位置,选择 GitHub 作为源提供商,并点击“连接到 Github”。如果要求,登录你的 GitHub 账户。

  5. 这将带你回到 AWS CodePipeline 页面。现在我们可以选择一个仓库和分支。我们将选择helloworld项目和主分支。点击“下一步”**。

如果你没有看到组织名/仓库名(即yogeshrahejahelloworld/helloworld),那么作为变通方法,可以将组织名/仓库名克隆/复制到你的全球 GitHub 仓库(即,将yogeshrahejahelloworld/helloworld改为yogeshraheja/hellworld,这是我的情况)。

  1. 这将带我们进入管道的第三阶段,在这里我们可以选择我们的构建提供商。我们的应用程序是用 Node.js 编写的,所以我们不需要构建任何东西。选择“不构建”并点击“下一步”。

  2. 下一个步骤叫做 Beta。这实际上是我们的暂存部署步骤。在部署提供者下,选择 AWS CodeDeploy。在应用程序名称下,选择 helloworld。最后,选择暂存环境作为部署组。点击下一步。

  3. 这将带我们进入一个步骤,在这里我们需要选择角色名称。方便的是,AWS 还添加了一个创建角色按钮。点击这个按钮。

  4. 在下一屏幕上,选择创建一个新的 IAM 角色,并将其命名为 AWS- CodePipeline-Service。使用推荐的策略并点击允许。

  5. 回到 CodePipeline 步骤,确保角色名称显示为 AWS- CodePipeline-Service。点击下一步。

  6. 在审核屏幕上,确保一切正确。最后,点击创建流水线。

由于我们使用的是 Web 界面,Amazon 会自动为您创建一个 S3 存储桶,以便存储流水线运行时生成的工件。

流水线将在几秒钟内创建并首次运行。

为了演示 CodeDeploy 和 CodePipeline 的基本功能,我们使用了 Web 和命令行界面。这个过程非常手动,并没有经过任何形式的审查过程。CloudFormation 支持这两项服务。对于实际的生产系统来说,最好使用类似 Troposphere 的工具来编程生成模板,从而管理这些服务,而不是手动进行更改。

一旦两个步骤都执行完成,您可以通过在浏览器中打开 http://**<instanceip>**:3000 来验证代码是否已经部署。实例的 IP 地址可以在 CloudFormation 模板或 EC2 控制台中找到。您甚至可以使用以下命令行验证成功:

$ aws cloudformation describe-stacks \
 --stack-name helloworld-staging \
 --query 'Stacks[0].Outputs[0].OutputValue' \
 | xargs -I {} curl {}:3000 
Hello World

我们已经完成了基础流水线的创建。通过利用 CodePipeline、CodeDeploy、GitHub 和 S3,我们构建了一个非常优雅的解决方案来处理我们 web 应用程序的部署。每当一个拉取请求被合并到主分支时,我们的流水线会自动获取变更,创建一个包含新代码的新包,将其存储在 S3 上,然后部署到暂存环境。感谢 CodeDeploy,我们可以进行基本的测试以验证版本是否正常工作。如有需要,我们还可以回滚到之前构建的任何版本。

我们的流水线不必仅限于暂存环境;我们实际上可以做更多的事情。正如我们之前提到的,CodePipeline 可以与 Jenkins 集成。我们可以使用 CodePipeline 来构建工件,也可以执行一些额外的测试序列。让我们在部署到暂存环境之前将其添加到我们的流水线中。

将 Jenkins 集成到我们的 CodePipeline 流水线中

Jenkins 受欢迎的一个特点是其插件功能。AWS 发布了多个插件,以将不同的服务与 Jenkins 集成。我们将使用为 CodePipeline 创建的插件。首先,这需要我们更改实例的 IAM 配置文件角色,以便它可以与 CodePipeline 交互。然后,我们将在 Jenkins 中安装 CodePipeline 插件,并创建一个作业来运行我们的测试。最后,我们将编辑管道以集成新的阶段。

通过 CloudFormation 更新 IAM 配置文件

为了将新权限添加到实例配置文件中,我们将编辑在本章早些时候创建的jenkins-cf-template.py模板。我们将添加一个策略,以授予 Jenkins 实例与 CodePipeline 通信的权限。这一步非常类似于我们之前为 Web 服务器授予 S3 访问权限的更改。

在实例变量实例化的上方,添加以下内容:

t.add_resource(IAMPolicy(
    "Policy",
    PolicyName="AllowS3",
    PolicyDocument=Policy(
        Statement=[
            Statement(
                Effect=Allow,
                Action=[Action("s3", "*")],
                Resource=["*"])
        ]
    ),

))

然后,保存更改并重新生成模板。新模板应如下所示:raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter05/EffectiveDevOpsTemplates/jenkins-cf-template.py

$ git add jenkins-cf-template.py
$ git commit -m "Allowing Jenkins to interact with CodePipeline"
$ git push
$ python jenkins-cf-template.py > jenkins-cf.template 

使用 Web 界面更新堆栈:

  1. 打开console.aws.amazon.com/cloudformation

  2. 勾选 Jenkins 堆栈旁边的复选框,然后在操作菜单中选择更新堆栈。

  3. 浏览到新生成的jenkins-cf.template并点击下一步,直到您进入审阅页面:

  1. 如前所示,只有 IAM 策略被添加,因为我们是通过实例配置文件创建实例的。我们的 EC2 实例将保持不变,这使得此更改是安全的。点击更新以确认更改。

实例策略将被更新,赋予 Jenkins 足够的权限与 CodePipeline 交互。现在我们可以安装 CodePipeline 的 Jenkins 插件了。

安装并使用 CodePipeline Jenkins 插件

在 Jenkins 中安装插件非常简单:

  1. 在浏览器中打开您的 Jenkins 实例(在我的例子中是http://18.208.183.35:8080)。

  2. 如果需要,登录并点击管理 Jenkins。

  3. 在管理 Jenkins 页面,选择管理插件。

  4. 搜索名为 AWS CodePipeline Plugin 的插件,选择它并安装。我们现在可以开始使用这个插件了。

  5. 返回到 Jenkins 服务器的主页。

  6. 点击左侧菜单中的新建项。

  7. 给新项命名为HelloworldTest,选择 Freestyle 项目**,然后点击页面底部的 OK 按钮。

  8. 在下一个屏幕上,在 Source Code Management 部分选择 AWS CodePipeline。因为我们在实例配置文件级别配置了权限,所以我们需要配置的唯一选项是 AWS Region 和 Category,在我们的案例中分别是 US_EAST_1Test

  9. 在 Build Triggers 下,选择 Poll SCM,然后输入 * * * * *,以告知 Jenkins 每分钟检查一次 CodePipeline,看看是否有可能的代码测试请求。

  10. 在 Build 部分,点击 Add build step 然后选择 Execute shell。

  11. 我们将再次运行在本章开始时创建的测试。在 Command 部分,输入以下内容:

npm config set registry http://registry.npmjs.org/ 
npm install
./node_modules/mocha/bin/mocha 
  1. 添加一个构建后操作,并选择名为 AWS CodePipline Publisher 的操作。

  2. 在新生成的 AWS CodePipeline Publisher 中,点击 Add,并将 Location 保留为空。

  3. 你可以根据自己的偏好配置其余的作业,然后点击 Save 来创建新作业。

我们在 Jenkins 中的测试作业已经准备好使用,现在我们可以更新我们的流水线。

向我们的流水线添加一个测试阶段。

我们将使用 Web 界面来进行这个更改:

  1. 在浏览器中打开 console.aws.amazon.com/codepipeline

  2. 选择我们之前创建的 helloworld 流水线。

  3. helloworld 流水线页面上,点击流水线顶部的 Edit 按钮。

  4. 点击 Source 和 Beta 阶段之间的 + Stage 按钮来添加一个阶段。

  5. 将该阶段命名为 Test,然后点击 Action。

  6. 在右侧菜单的 Action category 下,选择名为 Test 的操作。

  7. 将你的操作命名为 Jenkins,并且对于 Test provider,选择 Add Jenkins。

  8. 在 Add Jenkins 菜单中,将 Provider Name 设置为 Jenkins。提供你的 Jenkins URL,我这里是 http://18.203.183.35:8080。项目名称需要与 Jenkins 上作业的名称匹配。这个名称应该是 HelloworldTest。设置完成后,点击 Add action。

  9. 通过点击流水线顶部的 Save pipeline changes 来应用你的更改。

  10. 再次点击 Release change 运行流水线。几分钟后,你应该能看到 Jenkins 步骤正在执行。如果一切顺利,它应该会变成绿色。

我们的流水线现在开始变得非常有趣。在这里,我们展示了 Jenkins 集成的最基础形式,但你可以轻松想象出更现实的场景,比如在将代码部署到暂存环境后,添加一个步骤来进行更好的验证,包括更完善的集成、负载测试,甚至是渗透测试。

AWS CodePipeline 的目标是帮助你将服务从源控制一直推进到生产环境。刚开始处理一个服务时,你可能没有足够的测试覆盖来持续部署到生产环境,因此你可能会选择一键生产部署。我们将利用本章中迄今为止构建的自动化,构建一个用于生产的持续交付流水线。

为生产环境构建持续交付管道

为了构建我们的持续交付管道,我们将首先为生产环境创建一个 CloudFormation 堆栈。然后,我们将在 CodeDeploy 中添加一个新的部署组,这将使我们能够将代码部署到新的 CloudFormation 堆栈。最后,我们将升级管道,加入一个批准流程,以便将代码部署到生产环境,并加入生产环境的部署阶段。

为生产环境创建新的 CloudFormation 堆栈

在这里,我们将重新使用与之前用于预发布环境相同的模板。在你的终端中,进入你用于生成节点服务器模板的位置,然后运行与之前相同的命令,但这次使用堆栈名称helloworld-production

$ aws cloudformation create-stack \
 --capabilities CAPABILITY_IAM \
 --stack-name helloworld-production \
 --template-body file://nodeserver.template \
 --parameters ParameterKey=KeyPair,ParameterValue=EffectiveDevOpsAWS

然后我们可以运行以下命令,等待堆栈准备就绪:

$ aws cloudformation wait stack-create-complete \
 --stack-name helloworld-production

你可能会意识到,我们的生产堆栈中只有一个 EC2 实例的弱点。我们将在第六章《扩展你的基础设施》中讨论这一问题,届时我们将讨论扩展策略。

创建一个 CodeDeploy 组以便部署到生产环境

之前,我们创建了一个 CodeDeploy 应用程序和第一个部署组,允许我们将代码部署到预发布环境。现在,我们将使用命令行界面,添加一个新的部署组,将代码部署到新创建的生产环境。

添加新部署组所需的参数之一是我们最初创建的策略的arn。我们可以轻松地从之前创建的预发布部署组中提取这一信息。我们将把结果存储在一个名为arn的变量中:

$ arn=$(aws deploy get-deployment-group \
 --application-name helloworld \
 --deployment-group-name staging \
 --query 'deploymentGroupInfo.serviceRoleArn')

我们现在可以运行以下命令来创建新的部署组:

$ aws deploy create-deployment-group \
 --application-name helloworld \
 --ec2-tag-filters Key=aws:cloudformation:stack-             
    name,Type=KEY_AND_VALUE,Value=helloworld-production \
 --deployment-group-name production \
 --service-role-arn $arn

如果一切顺利,新的部署组应该已经创建。我们可以通过浏览 AWS CodeDeploy 网页上的应用程序,或使用以下命令行命令来验证这一点:

$ aws deploy list-deployment-groups \
 --application-name helloworld
{
"applicationName": "helloworld", 
"deploymentGroups": [
 "staging",
        "production"
]
} 

向我们的管道添加持续交付步骤

正如我们在本章前面看到的,管道由多个阶段组成。在 CodePipeline 中,阶段由其类别来定义。到目前为止,我们已经探索了三种类别:源、部署和测试。为了向管道中添加一个确认步骤,以便将我们的服务部署到生产环境中,我们将使用一个新的类别,叫做批准

批准操作提供了多个配置选项,用于在任务等待批准时发送通知。为了演示此功能,我们将创建一个新的 SNS 主题并订阅它。正如你在第三章《将基础设施视为代码》中记得的那样,SNS 是我们用来监控基础设施的简单通知服务。

我们将使用命令行创建一个新的主题并订阅它:

$ aws sns create-topic --name production-deploy-approval
{
"TopicArn": "arn:aws:sns:us-east-1:511912822958:production-deploy- approval"
}

在这里,我们将使用电子邮件订阅。SNS 还支持其他多种协议,如 SMS、HTTP 和 SQS。为了订阅,你需要知道主题 ARN,它在前一个命令的输出中:

$ aws sns subscribe --topic-arn \
 arn:aws:sns:us-east-1:511912822958:production-deploy-approval \
 --protocol email \
 --notification-endpoint yogeshraheja07@gmail.com
{
"SubscriptionArn": "pending confirmation"
} 

去你的收件箱确认订阅。

我们现在可以添加新的阶段,从批准阶段开始:

  1. 在浏览器中打开 console.aws.amazon.com/codepipeline

  2. 选择 helloworld 应用程序。

  3. 点击流水线顶部的 编辑

  4. 点击流水线底部 Beta 阶段下方的 + 阶段 按钮。

  5. 给它命名为 Approval

  6. 点击 + 操作

  7. 在操作类别菜单中选择 Approval。

  8. 将此操作命名为 Approval。

  9. 选择批准类型手动批准

  10. 选择我们刚创建的 SNS 主题。输入 production deploy 应该能让你轻松找到该主题,因为表单的自动完成功能。

  11. 最后,点击添加操作。现在,我们将添加生产部署步骤到此批准下方。

  12. 点击新创建的阶段 Approval 下方的 + 阶段 按钮。

  13. 将这个新阶段命名为 Production。

  14. 点击 + 操作。

  15. 选择部署类别。

  16. 将此操作命名为 Production。

  17. 选择 CodeDeploy 提供程序。

  18. 选择 helloworld 作为我们的应用名称。

  19. 选择部署组 production。

  20. 选择工件 MyApp

  21. 点击添加操作。

  22. 通过点击流水线顶部的保存流水线更改,完成创建我们的新阶段。

我们可以再次点击发布变更来测试更新后的流水线。

流水线将通过前三个阶段,然后在批准阶段暂停。如果你查看电子邮件收件箱,会发现一个链接,点击链接可以审查变更。或者,你也可以使用 Web 界面,在批准阶段点击审查按钮:

在仔细审查变更后,你可以批准或拒绝该变更。如果被批准,部署将继续进行到流水线的最后一步,将代码部署到生产环境。

我们现在已经自动化了整个发布过程。我们的 helloworld 应用程序可能无法完全代表一个真实的应用程序,但我们围绕它构建的流水线确实可以。我们所构建的可以作为从一个环境到另一个环境安全部署更复杂应用程序的蓝图。

毫无疑问,快速移动并将新的功能和服务发布给客户的能力,可以帮助你避免中断。构建持续部署流水线的最后一步是去除手动批准流程,以便将代码发布到生产环境,从而去掉发布过程中涉及的最后一步人工操作。多年来,不同的公司提出了几种策略,使生产部署成为一个安全的过程。在接下来的部分,我们将看看一些你可以实施的解决方案。

在生产中实践持续部署的策略

和往常一样,你的第一道防线是拥有足够的测试覆盖率和复杂的验证脚本,涵盖产品中大多数敏感的路由和功能。有一些广为人知的策略和技术可以使持续部署流水线在生产环境中更为安全。我们将在本节中探索三种常见的策略。

快速失败

我们构建的流水线相当快速且健壮。根据服务的性质,你可以选择相信你团队编写的代码质量,并始终将代码直接部署到生产环境。通过充分监控日志和应用程序指标,你将能够在代码部署后的几分钟内捕捉到问题。然后,你可以依赖 CodeDeploy 及其快速部署旧版本的能力来从这种情况中恢复。

如果你采取这种方式并检测到问题,直接回滚到之前的版本即可。你可能完全知道问题所在,并且知道修复起来很容易,但由于知道问题正在影响用户,所产生的压力可能会导致你犯更多错误,使得情况变得更糟。

金丝雀发布

类似地,你也可以尝试将代码直接部署到生产环境,但只将一部分流量暴露给新代码一段时间。你可以构建一个系统,让只有一小部分流量访问运行新代码的新服务器,并在短时间内对比每个版本的错误率和性能。通常,10% 的流量在 10 分钟内就足以收集到有关新构建质量的信息。如果过了这段时间后,一切看起来正常,那么你就可以将 100% 的流量切换到新版本的服务。

内存泄漏等 Bugs 通常表现较慢;一旦部署完成,继续密切监控不同的系统和关键指标,确保没有出现任何错误:

特性开关

这种策略也被称为“暗发布”,是最难实现但也是最有价值的策略。大多数知名科技公司都在使用这种策略。其核心思路是,在每个功能上都设置多个智能开关。当你首次部署新功能的代码时,开关是关闭的。接着,你逐步为不同的用户子集打开这些开关。你可以首先仅允许公司员工体验这个功能。接下来,你可能会通过添加一组信任的用户,增加体验该功能的人数。然后,你可能会为 20% 的用户开启该功能,再到 50%,依此类推。除了允许你进行软发布外,这种功能还可以在产品层面进行 A/B 测试、维护操作(比如关闭特定功能),甚至进行负载测试。

Facebook 在其博客中总结了暗启动的最佳应用之一。2008 年,Facebook 推出了他们的聊天功能。这是一个非常具有挑战性的功能,因为它是 Facebook 首个使用 Erlang 开发的服务。为了确保该服务能够处理 Facebook 运营规模的流量,他们依赖于暗启动策略。在正式发布之前的几个月里,他们通过发布没有 UI 的服务,模拟了真实流量的情况。真实用户的浏览器会与聊天服务器建立连接,隐形地发送和接收消息以模拟负载。当发布时,Facebook 并没有推出新代码,而只是简单地打开开关,使聊天窗口在 UI 中可见。有关此次发布的更多信息,请参见:www.facebook.com/notes/facebook-engineering/facebook-cha t/14218138919/

总结

在本章中,我们深入探讨了 DevOps 思想中的一个重要方面——如何改变代码发布的方式。

我们的第一个目标是提高开发人员的生产力。为此,我们构建了一个持续集成管道。利用 Jenkins 和 GitHub,我们创建了一个新的工作流,开发人员在各自的分支中提交代码,并提交拉取请求。这些分支会通过 Jenkins 自动进行测试,最终的同行评审确保提交的代码质量高。

多亏了这个变更,我们可以保证项目的主分支中的代码始终是良好的,值得推送到暂存环境。为了做到这一点,我们构建了一个持续部署管道。得益于 AWS CodeDeploy 和 CodePipeline,我们能够轻松构建一个功能完整的管道。该管道具备了操作员所期望的所有特性。它会自动捕捉开发人员合并拉取请求的更改,创建新版本应用的包,将该包存储在 S3 中,然后部署到暂存环境。随着新代码的部署,验证步骤确保应用不会出现异常,并且如果需要,应用可以轻松回滚。

一旦我们完成了持续部署管道的构建,我们将其扩展为持续交付功能,以便能够按需进行生产部署。我们还在管道中添加了一个额外的阶段,通过 Jenkins 集成测试。最后,我们讨论了不同的技术和策略,以建立一个适用于生产环境的持续部署管道,这将使我们能够每天对任何给定的服务进行几十次生产部署。

自从我们开始采取更偏向 DevOps 的方式来管理我们的架构和服务后,我们就没有关注过高可用性或负载均衡的概念。即使在本章中,我们也只为生产环境创建了一个 EC2 实例。我们将在第六章,扩展你的基础设施中讨论这一问题。我们将探讨扩展基础设施的工具和服务,并处理海量流量。

问题

  1. 什么是持续集成、持续部署和持续交付?

  2. 什么是 Jenkins,它如何在 SDLC 周期中提供帮助?

  3. 描述如何构建你的第一个持续部署流水线。

进一步阅读

请阅读以下文章以获取更多信息:

第六章:扩展你的基础设施

本章将分析用于在Amazon Web ServicesAWS)中部署完整 Web 应用程序的所有技术。特别是,我们将研究如何在单台机器上创建单体应用程序,并将应用程序拆分为多个部分,以实现可扩展性和可靠性。

本章的每个部分首先都有一个理论部分,重点介绍概念的整体思想以及实现所需的 AWS 技术。它还包括一个实际示例,使读者能够将所讲解的内容付诸实践

从单体方法开始,将所有软件集中在一台机器上,我们将看到在何时何种情况下,将其拆分为多个部分以实现更好的可扩展性和可靠性是方便的。为了做到这一点,将数据(也称为应用程序的状态)移出 EC2 机器是可以使用 RDS(Amazon 云中的数据库服务)完成的第一步。添加负载均衡器可以带来许多优势,从使用AWS 证书管理器ACM)到准备基础设施和实现横向扩展。配置自动扩展组/启动配置是为我们的应用程序启用横向扩展的最后一步。

技术要求

本章假定具有 AWS 控制台的基础知识。这些内容在前几章中已经覆盖,以及在第四章中完成的 Terraform 配置中提到,使用 Terraform 进行基础设施即代码

在 AWS 账户中提供一个公共域。这对于测试 Web 应用程序的各个方面非常有用,但这只是一个可选步骤。

还需要具备 Linux 命令行工具的基础知识,因为该示例是在 Amazon Linux 2 操作系统上构建的。本章中包含的代码文件可以在 GitHub 上找到,链接:github.com/giuseppeborgese/effective_devops_with_aws__second_edition

一个单体应用程序

本章的目的是引导读者将通常称为单体 应用程序的内容,转变为一个动态且可扩展的应用程序。

什么是单体应用程序?

当人们谈论扩展时,他们通常会使用单体应用程序这个术语。那么,这到底是什么呢?通常,这指的是一个软件或基础设施,其中所有内容(包括展示部分、后端和数据部分)都被合并为一个单一模块,称为单体。在我们的例子中,我们关注的是基础设施。为了说明单体应用程序的概念,我们将构建一个包含以下组件的示例应用程序,如下图所示:

  • 一个 MySQL 数据库,只有一个表,里面有一个数字字段

  • 一个后端前端 Java/Tomcat 服务,监听默认的 8080 端口,读取数据库,显示数值并增加数值

  • 一个监听默认端口 80 的 Apache 2.2 Web 服务器,与 Tomcat 通信并显示网页

  • 所有内容都包含在一个分配了公网 IP 地址的单个 EC2 虚拟机中,以便在互联网上进行通信

让我们创建一个示例应用程序,允许我们进行拆分和扩展:

我们在前面的章节中学习了如何使用 Terraform。为了构建 EC2 实例和安全组,可以使用名为 monolith application 的模块。如要在你的账户中使用它,需要更改初始化参数并提供你的个人信息:* vpc-id * subnet * pem 密钥。至于 AMI,你可以根据以下截图中的提示找到合适的 AMI。这个示例是在北弗吉尼亚 Amazon 区域和 Amazon Linux 2 操作系统上测试的。找到你所在区域的 AMI ID,如以下截图所示:

module "monolith_application" {
  source = "github.com/giuseppeborgese/effective_devops_with_aws__second_edition//terraform-modules//monolith-playground"
  my_vpc_id = "${var.my_default_vpcid}"
  my_subnet = "subnet-54840730"
  my_ami_id = "ami-04681a1dbd79675a5"
  my_pem_keyname = "effectivedevops"
}

创建模块的命令总是如下所示:

terraform init -upgrade
terraform plan -out /tmp/tf11.out -target module.monolith_application
terraform apply apply /tmp/tf11.out

你应该在输出中得到以下结果:

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
monolith_url = http://54.209.174.12/visits

如果你等几分钟,让应用程序运行并安装所有软件和配置,你可以在浏览器中输入 URL,看到如下截图所示的内容:

如果你收到这个结果,你需要等待几分钟。如果这不能解决错误,可能在安装过程中出了问题。错误信息如下:

当然,你的公网 IP 地址会与此不同。每次刷新页面或从任何来源打开 URL 时,Java 应用程序都会从 MySQL 数据库读取数值,增加 1 单位的值,并写入相同的数据库字段。

值得写几行代码来查看一切是如何安装的。这个代码位于上面显示的 monolith_application 模块中:

以下是 monolith_application 的安装脚本:

yum -y install httpd mariadb.x86_64 mariadb-server java

systemctl start mariadb
chkconfig httpd on
chkconfig mariadb on
systemctl restart httpd

现在安装 MySQL(MariaDB)——这是 Amazon Linux 2 长期支持LTS)默认仓库中提供的 MySQL 类型,同时也包含 Apache 2 和 Java 软件。

以下是我们之前开始解释的安装脚本:

echo "<VirtualHost *>" > /etc/httpd/conf.d/tomcat-proxy.conf
echo " ProxyPass /visits http://localhost:8080/visits" >> /etc/httpd/conf.d/tomcat-proxy.conf
echo " ProxyPassReverse /visits http://localhost:8080/visits" >> /etc/httpd/conf.d/tomcat-proxy.conf
echo "</VirtualHost>" >> /etc/httpd/conf.d/tomcat-proxy.conf

Apache 已配置为将流量转发到 8080 端口上的 Tomcat。

为了以非交互方式设置 MySQL,我使用了以下几行代码来为 Java 应用程序创建数据库、表和用户:

mysql -u root -e "create database demodb;"
mysql -u root -e "CREATE TABLE visits (id bigint(20) NOT NULL AUTO_INCREMENT, count bigint(20) NOT NULL, version bigint(20) NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=latin1;" demodb
mysql -u root -e "INSERT INTO demodb.visits (count) values (0) ;"
mysql -u root -e "CREATE USER 'monty'@'localhost' IDENTIFIED BY 'some_pass';"
mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'monty'@'localhost' WITH GRANT OPTION;"

user_data 脚本位于 module_application 中,并作为参数提供给 user_data 字段。它下载一个示例 Java 应用程序,将结果保存到数据库中。为了简化安装,.jar 文件中还包含了 Tomcat。这对于实验环境是可以接受的,但不适合实际使用:

runuser -l ec2-user -c 'cd /home/ec2-user ; curl -O https://raw.githubusercontent.com/giuseppeborgese/effective_devops_with_aws__second_edition/master/terraform-modules/monolith-playground/demo-0.0.1-SNAPSHOT.jar'
runuser -l ec2-user -c 'cd /home/ec2-user ; curl -O https://raw.githubusercontent.com/giuseppeborgese/effective_devops_with_aws__second_edition/master/terraform-modules/monolith-playground/tomcat.sh'
cd /etc/systemd/system/ ; curl -O https://raw.githubusercontent.com/giuseppeborgese/effective_devops_with_aws__second_edition/master/terraform-modules/monolith-playground/tomcat.service
chmod +x /home/ec2-user/tomcat.sh
systemctl enable tomcat.service
systemctl start tomcat.service

为了让 Tomcat 在启动时作为服务运行,.jar 文件和配置文件会被下载,并且配置会自动完成。

无论如何,playground 应用程序的目的是保存其计算结果(称为状态)到数据库中。每次访问该 URL 时,状态会从数据库中读取、递增并重新保存。

关联一个 DNS 名称

这对练习来说不是必须的,但如果你有公共域名注册,你可以创建一个 A 记录

你需要像我一样注册一个 Route 53 公共域名:devopstools.link。如果你不知道如何注册,可以访问docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register-update.html并按照那里的说明进行操作。根据我的经验,你需要等待大约 30 分钟到两个小时,新域名就可以使用了。要创建记录,请按照以下步骤操作:

  1. 前往 Route53 | 托管区,选择你的区域

  2. 点击“创建记录集”按钮

  3. 输入一个名称并选择 bookapp 名称

  4. 输入你的 EC2 机器的公网 IP 地址

  5. 点击“创建”按钮,如下图所示:

  1. 现在你可以使用此记录来查询应用程序:

扩展单体应用程序

我们现在已经创建了基础设施并部署了应用程序,它运行良好。如果这个应用程序对大量用户有用,用户数量、请求量和数据量有可能会迅速增长。这正是每个应用程序所有者所希望的。

可能我们选择的 EC2 已经无法有效管理大量数据。也有可能出现以下情况:

  • CPU 或内存不足以支持我们的三大程序:Apache、Tomcat 和 MySQL

  • EC2 虚拟机的带宽不足以处理大量的并发请求

  • Tomcat 或 MySQL 需要为每个用户存储数据,而磁盘空间已不足以支持更多数据。

  • MySQL 和 Tomcat 需要同时从单个磁盘读取大量数据。此外,单个磁盘还需要进行上下文切换。

扩展应用程序有两种方式,分别是:

  • 垂直扩展,这意味着使用更强大的 EC2 实例,从而获得更多的 CPU、更大的内存和更好的网络性能

  • 水平扩展,这意味着添加更多的 EC2 实例,同时运行相同的代码,并将流量负载均衡地分配到这些实例上

现在我们有的是单体架构,所以我们只能进行垂直扩展。在接下来的部分,我们将把单体架构拆分成不同的部分,将状态从 EC2 虚拟磁盘中移除。这样,我们可以添加更多的机器,并且使用 CDN 在负载均衡器和数据库之间分配负载。

要对单体架构进行垂直扩展,您需要按照以下步骤进行:

  1. aws.amazon.com/ec2/instance-types/列表中选择一个新的实例类型。

  2. 关闭实例。

  3. 更改实例类型:

  1. 开启实例。

对于磁盘空间而言,这有点复杂。这里,你必须扩展大小。具体步骤如下:

  1. 关闭机器以避免日期不一致。

  2. 分离每个附加到实例的卷。然而,在此之前,请记录所使用的设备:/dev/sda1/dev/xdc 等等。

  3. 为每个附加到实例的卷创建快照。

  4. 对于前一步创建的每个快照,创建一个新卷。你需要指定卷的所需大小。

  5. 使用与第 2 步相同的设备名称,将每个新卷附加到实例。

  6. 开启机器。

  7. 登录到机器并使用 Linux 和 Windows 的指南调整文件系统大小。有关更多细节,请参阅章节末尾的进一步阅读部分。

单体架构的优点

在拆分并扩展我们的单体架构之前,重要的是要了解是否值得为我们的应用程序付出努力。让我们来看看单一块架构的所有优点:

  • 第一个优点肯定是基础设施成本。在这里,我们将其拆分成多个可扩展的部分,但这意味着我们需要为架构的每个部分付费。最终的基础设施成本将高于单一的单体架构。

  • 构建一个多层可扩展架构的成本肯定比单体架构要复杂得多。这意味着需要更多的时间来构建,并且需要更多的技能来完成。此书的目的之一就是缩小这些技能差距。

  • 一个分布式架构需要许多设置。例如,正确配置安全组,选择合适的负载均衡器,选择合适的 RDS,以及配置 S3 或 EFS 以将状态从虚拟磁盘中移出。唯一的例外是 SSL 配置。使用 AWS 证书管理器配置 SSL 比购买并为 Apache 配置 SSL 证书要容易得多。

所以,如果你预期流量不大,预算有限,你可以考虑构建一个单体架构来托管你的 Web 应用程序。当然,请记住,当你想垂直扩展时,需要接受可扩展性限制和停机时间。

数据库

现在我们已经了解了单体应用的优缺点,并决定将应用拆分成多个部分,是时候将第一个资源移出单体应用了。

正如我们在本章第一节中预期的那样,必须将数据(也称为状态)移出 EC2 机器。在某些 web 应用中,数据库是唯一的数据源。然而,在其他一些应用中,还可能有从用户上传的文件直接保存到磁盘上的数据,或者如果使用如Apache Solr这样的索引引擎,也可能会有索引文件。有关更多信息,请参阅lucene.apache.org/solr/

如果可能的话,使用云服务总是比在虚拟机中安装程序更方便。对于数据库,RDS 服务(aws.amazon.com/rds/)提供了一个大型的开源或闭源数据库集合(Amazon Aurora、PostgreSQL、MySQL、MariaDB、Oracle 和 Microsoft SQL Server),因此,如果你需要IBM Db2www.ibm.com/products/db2-database,你可以使用 RDS 服务来托管你的数据库。

要创建我们的 MySQL RDS 实例,请参考官方注册表中的模块:registry.terraform.io/modules/terraform-aws-modules/rds/aws/1.21.0:

需要注意的是,在拆分应用时,必须正确配置安全组,以允许从 EC2 实例通过端口3306访问 RDS 实例。这也有助于避免不必要的数据库访问。

对于子网,必须为 EC2 实例保留一个公共子网。而对于 RDS 实例,选择私有子网更加方便。我们将在第八章《硬化你的 AWS 环境的安全性》中深入探讨这一话题。

将数据库迁移到 RDS

要创建 MySQL 数据库,我们可以使用一个公共模块,该模块可在官方仓库中找到,链接如下:registry.terraform.io/modules/terraform-aws-modules/rds/aws/1.21.0

在以下代码中,我将稍微简化原始示例,并添加一个安全组,如下所示。请参阅main.tf文件:

resource "aws_security_group" "rds" {
  name = "allow_from_my_vpc"
  description = "Allow from my vpc"
  vpc_id = "${var.my_default_vpcid}"

  ingress {
    from_port = 3306
    to_port = 3306
    protocol = "tcp"
    cidr_blocks = ["172.31.0.0/16"]
  }
}

module "db" {
  source = "terraform-aws-modules/rds/aws"
  identifier = "demodb"
  engine = "mysql"
  engine_version = "5.7.19"
  instance_class = "db.t2.micro"
  allocated_storage = 5
  name = "demodb"
  username = "monty"
  password = "some_pass"
  port = "3306"

  vpc_security_group_ids = ["${aws_security_group.rds.id}"]
  # DB subnet group
  subnet_ids = ["subnet-d056b4ff", "subnet-b541edfe"]
  maintenance_window = "Mon:00:00-Mon:03:00"
  backup_window = "03:00-06:00"
  # DB parameter group
  family = "mysql5.7"
  # DB option group
  major_engine_version = "5.7"
}

the plan shows 5 these 5 resources to add 

  + aws_security_group.rds

  + module.db.module.db_instance.aws_db_instance.this

  + module.db.module.db_option_group.aws_db_option_group.this

  + module.db.module.db_parameter_group.aws_db_parameter_group.this

  + module.db.module.db_subnet_group.aws_db_subnet_group.this

Plan: 5 to add, 0 to change, 0 to destroy.

因为 RDS 需要在选项组、参数组和子网组中运行。

你可以在 RDS 控制台中看到新实例,并点击它以查看其属性,如下所示:

一旦打开所选实例的属性,注意记录 Endpoint 字段的值,如以下截图所示:

在我的例子中,这个资源是demodb.cz4zwh6mj6on.us-east-1.rds.amazonaws.com

通过 SSH 连接到 EC2 机器,并尝试连接到 RDS:

[ec2-user@ip-172-31-7-140 ~]$ mysql -u monty -psome_pass -h demodb.cz4zwh6mj6on.us-east-1.rds.amazonaws.com
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 5.7.19-log MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

运行show databases命令查看是否有demodb模式:


 MySQL [(none)]> show databases;
 +--------------------+
 | Database |
 +--------------------+
 | information_schema |
 | demodb |
 | innodb |
 | mysql |
 | performance_schema |
 | sys |
 +--------------------+
 6 rows in set (0.00 sec)
MySQL [(none)]> exit
Bye
[ec2-user@ip-172-31-7-140 ~]$

要转移数据库,请按照以下步骤操作:

  1. 使用pkill java 命令关闭 Java 进程

  2. 使用以下命令转储本地数据库:

 mysqldump -u monty -psome_pass -h localhost demodb > demodbdump.sql
  1. 我们不再需要本地数据库,因此使用以下命令停止它:
sudo service mariadb stop
  1. 现在使用以下命令在 RDS 中恢复转储:
 mysql -u monty -psome_pass -h demodb.cz4zwh6mj6on.us-east-1.rds.amazonaws.com demodb < demodbdump.sql
  1. 检查内容是否已正确复制,如下所示:
mysql -u monty -psome_pass -h demodb.cz4zwh6mj6on.us-east-1.rds.amazonaws.com
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 5.7.19-log MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> use demodb;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MySQL [demodb]> select * from visits;
+----+-------+---------+
| id | count | version |
+----+-------+---------+
| 1 | 5 | 5 |
+----+-------+---------+
1 row in set (0.00 sec)

现在,转储已正确,你需要在/home/ec2-user/tomcat.sh中替换连接:

sudo nano /home/ec2-user/tomcat.sh

现在在文件中找到字符串:

 db_url=jdbc:mysql://localhost:3306/

用以下代码行替换:

db_url=jdbc:mysql://demodb.cz4zwh6mj6on.us-east-1.rds.amazonaws.com:3306/

保持其他设置不变:

pkill java 
systemctl start tomcat

现在,你应该可以看到输出并且应用程序再次正常工作。

现在,可以方便地使用以下命令删除本地数据库:

sudo yum remove mariadb-server 

选择 RDS 类型

如果你有一个像我们在之前示例中看到的 MySQL 引擎,你可以从以下实例类型中选择:

在大多数情况下,MySQL 经典版是理想选择。然而,如果你知道将会有大量数据需要管理,那么 Aurora MySQL 是理想的选择。这个无服务器选项适用于不常用可变不可预测的工作负载。

备份

启用 RDS 实例的备份并选择 Windows 作为备份类型非常重要。当你预期数据库的写入负载较低时,这一点尤为重要,因为备份将会在没有停机的情况下完成,但它也可能影响性能。有关 Amazon RDS 最佳实践的更多信息,请参考docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_BestPractices.html

你可以设置每日备份,并保留最多 35 个快照。在恢复时,你可以选择这 35 个快照中的任意一个,或者选择这 35 天内的任何时刻,使用新的时间点恢复功能。有关更多信息,请参考docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PIT.html

多可用区

可用的多可用区(Multi-AZ)功能,详见aws.amazon.com/rds/details/multi-az/,在另一个可用区AZ)中使用主从技术维护 RDS 实例的第二个副本。如果主实例出现问题(或所在 AZ 的整个区域出现问题),DNS 名称将自动切换到从实例。使用此功能,两个 RDS 实例将始终保持在线。此外,成本将翻倍。因此,建议仅在生产环境中使用此功能。

在下图中,展示了一个多可用区架构:

ElastiCache

你可以考虑为你的数据库插入缓存,以减少 RDS 实例的负载。这将引入另一个基础设施组件,并且需要更改软件代码,以便使其能够使用缓存,而不仅仅是使用 RDS。根据你需要保存的数据类型,AWS 的 ElastiCache 服务可提供两种类型:aws.amazon.com/elasticache/RedisMemcached

弹性负载均衡器(ELB)

在本节中,我们将用 ELB 替换 Apache,并且还将添加一个 SSL 证书,如下图所示:

正如我们在前一节中为 RDS 所做的那样,在这里用一个托管服务替换安装在 EC2 机器上的软件是很方便的。

我们将从以下特性中受益:

  • 在多个可用区部署并提高可靠性

  • 一个 Web 界面来管理代理,而不是 Apache 配置文件

  • 一个完全可管理的服务,不需要执行软件升级

  • 可扩展性以处理请求(在某些情况下需要预热)

  • 方便将日志存储在 S3 桶中

或者,当你使用 ELB 时,需要遵循 AWS 方法,不能自由定制。Apache 是 Web 服务器的瑞士军刀;它有许多模块,使得可以执行各种不同的操作和任务。使用 ELB 时,可能会丧失一些有用的功能,比如从 HTTP 重定向到 HTTPS。

选择合适的 ELB

正如 AWS 文档中所述,aws.amazon.com/elasticloadbalancing,ELB 有两个版本和三种类型可供选择:

  • 版本 1:经典负载均衡器CLB

  • 版本 2:应用程序负载均衡器ALB)和网络负载均衡器NLB

每个产品可以按以下方式进行描述:

如果你想比较这三种产品的所有功能,可以查看 aws.amazon.com/elasticloadbalancing/details/#compare 上的对比表。不过,让我们尝试将这些差异总结如下:

  • 除非你有 EC2 经典网络,否则不应该再创建 CLB。在这种情况下,你应该尽快考虑迁移到 VPC 网络类型。你还需要了解 CLB,因为它是 AWS 云环境中最流行的产品。

  • 如果你需要管理 HTTP/HTTPS 连接——这适用于大多数 Web 应用程序——你应该使用 ALB。

  • 如果你需要管理 TCP 连接,或者需要控制负载均衡器的公网 IP,那么 NLB 是正确的选择。请记住,使用这种类型的负载均衡器时,无法使用 SSL 功能。

在我们的示例中,部署的正确负载均衡器是 ALB。这是因为我们希望使用 HTTP/S 协议的 Web 应用程序,并且希望在其中安装 SSL 证书。

部署负载均衡器

现在我们知道该做什么了,接下来是根据这些步骤调整我们的应用程序以适配负载均衡器:

  1. 配置安全组,允许从负载均衡器访问 EC2 机器的 8080 端口。ALB ==> 8080 EC2(我们指的是从应用程序负载均衡器到 EC2 机器的连接)。为了简化,我们将允许整个 VPC 无类域间路由 (CIDR) 访问。

  2. 创建 ALB,连接到 EC2 机器,并验证机器是否处于服务状态。

  3. 现在你可以将 DNS 记录从 EC2 机器的公网 IP 更改为负载均衡器的别名。

  4. 从机器中移除 Apache 软件;你不再需要它了。

在每个环境中,将负载均衡器部署到多个属于不同 AZ 的子网中是很方便的。记住,每个可用区就像一个数据中心,数据中心总是可能出现问题。多个可用区部署不会增加成本,和 RDS 不同,后者如果使用多 AZ 会双倍增加成本。

在 第五章,添加持续集成和持续部署 中,我们将使用 Terraform 来创建 ALB。在这里,我们将通过 Web 控制台执行这些更改,以便了解每个步骤的细节。

第一步 - 打开来自整个 VPC CIDR 的 8080 端口访问

如下所示,打开来自整个 VPC CIDR 的 8080 端口访问:

步骤 2 – 创建 ALB 并关联到 EC2 实例

创建 ALB 并将其关联到 EC2 实例,如下所示:

进入负载均衡器 | 创建负载均衡器,并在应用程序负载均衡器部分点击创建按钮,选择 ALB,如下图所示:

从方案部分选择面向互联网选项。这一点很重要,因为我们希望它可以从全球访问,同时我们还需要至少两个不同可用区(AZ)的子网:

忽略以下消息:

Improve your load balancer's security. Your load balancer is not using any secure listener.

接下来,我们将添加一个安全监听器。

为此,点击创建新安全组单选按钮,为此负载均衡器打开 HTTP 的端口 80

现在创建一个新的目标组。在该组中,请求会轮换并到达 EC2 实例。端口 8080 是 EC2 实例中 Tomcat 软件的端口。

我们的 playground 应用只有一个 URL,叫做 /visits,因此我们需要插入该 URL,每次健康检查执行时,数据库中的计数器会递增。在真实环境中,你需要一个通过读取数据库而非写入数据库来进行控制的健康检查,如下面的示例所示。在这个示例中,使用这种方法是可以接受的:

选择 EC2 实例并点击“添加到注册”按钮,如下图所示:

该实例将被添加到注册的目标列表中:

如果现在检查刚创建的目标组中的目标选项卡,你会看到你的实例和状态栏。如果状态在半分钟内没有变为健康,可能是配置出现了错误:

现在你可以检查负载均衡器的 URL,如下所示:http://break-the-monolith-939654549.us-east-1.elb.amazonaws.com/visits

再次提醒,你的 URL 会与此不同,但此时你应该能理解它是如何工作的。

步骤 3 – 为 ELB 创建别名

进入新的 Route 53 区域,并用 CNAME 别名修改之前创建的 A 记录,如下图所示:

在不到 300 秒的时间内,你应该能看到更改,且 DNS 会指向新的域名。

步骤 4 – 从机器上移除 Apache 软件

此时,我们不再需要 EC2 实例中的 Apache 软件。要移除它,请运行以下命令:

sudo yum remove httpd

同时,清理 EC2 实例的安全组也很方便,可以移除对端口 80 的访问:

保持 SSH 对你的 IP 开放,选择我的 IP 来源选项。

配置 SSL 证书

你可以配置一个仅适用于某一 DNS 记录的证书,例如 example.devopstools.link,或者配置一个通用的证书,如 *.devopstools.link,它适用于每个子域。我的建议是使用 *,这样每次添加新资源时就不需要重复执行证书配置过程了。

证书管理器使得你可以免费获取 SSL 证书,除非你使用的是私有证书颁发机构。按照这些步骤生成 SSL 证书:

进入 AWS 证书管理器服务,并点击如下截图中所示的“Provision certificates”部分:

选择“Request a public certificate”选项,如下所示:

现在输入域名。在我的案例中,这包括域名和带有 * 的域名:

我决定使用 DNS 验证选项,但电子邮件验证选项也是可以的。在这种情况下,你需要访问用于注册域名的电子邮件地址:

向导提示你为我们在开始时插入的每个域名创建一个 DNS 记录。在我们的案例中,这指的是两个域名(*.devopstools.linkdevopstools.link)。你可以按照向导的提示点击“Create record in Route 53”按钮来创建记录,如下截图所示:

点击“Create”按钮为两个 DNS 记录创建证书。此时,创建的记录将显示出来:

不到一分钟,新的 SSL 证书状态将显示为“已颁发”(Issued),并且可以开始使用:

如果你以前创建过 SSL 证书,你会知道与传统方法相比,这个过程是多么简单和直接。你现在可以将新证书添加到负载均衡器中,并使用 SSL 监听器。

首先,你需要为新端口 443 打开 ALB 的安全组,如下截图所示:

进入你的负载均衡器,然后点击“Listeners”选项卡,再点击“Add listener”按钮,如下所示:

  • 选择 HTTPS 协议和其默认端口 443

  • 规则是将流量转发到创建时已定义的目标

  • 最后,从“From ACM (recommended)”下拉菜单中选择之前创建的证书,如下所示:

现在,你已经为应用程序创建了一个安全证书,如下所示:

ALB 和与 Auth0 的集成

如果您希望在用户访问由负载均衡器提供的内容之前进行身份验证,那么您可以将 ALB 与 auth0.com/ 提供的 Auth0 服务集成。这是一个云服务,旨在通过不同的身份验证方式管理用户,并提供一个通用的身份验证和授权平台,适用于 Web、移动和遗留应用程序。

如果您想尝试这个有趣的配置功能,请按照 medium.com/@sandrinodm/securing-your-applications-with-aws-alb-built-in-authentication-and-auth0-310ad84c8595 中的指南操作。

预热负载均衡器

CLB 的一个著名问题是,为了管理流量峰值,需要进行预热,因为该系统是设计为扩展的,正如您在文档中可以看到的那样。我们建议您每五分钟以不超过 50% 的速率增加负载。

关于这个主题的官方声明可以在 aws.amazon.com/articles/best-practices-in-evaluating-elastic-load-balancing/#pre-warming 找到。声明中提到以下内容:

"Amazon ELB 能够处理我们客户绝大多数的使用案例,而无需进行“预热”(即根据预期流量配置负载均衡器,以确保其具有适当的容量)。在某些情况下,例如预期会有突发流量,或无法配置负载测试以逐步增加流量时,我们建议您联系我们 aws.amazon.com/contact-us/,以便对您的负载均衡器进行“预热”。我们将根据您预期的流量配置负载均衡器的适当容量。我们需要了解您的测试或预期的突发流量的开始和结束日期、每秒的预期请求速率以及您将测试的典型请求/响应的总大小。"

ALB 和 NLB 之间的区别:

  • NLB 旨在处理每秒数千万的请求,同时保持超低延迟的高吞吐量,且无需客户进行额外操作。因此,不需要预热。

  • ALB 则遵循与 CLB 相同的规则。

  • 简而言之,NLB 不需要预热。然而,CLB 和 ALB 仍然需要预热。

访问/错误日志

配置 ELB 将访问/错误日志存储到 S3 存储桶中是一个良好的实践:

下一步

现在,我们已经在多可用区(multi-AZ)配置了负载均衡器并启用了 SSL,同时系统是可扩展的,RDS 也部署在多可用区。然而,EC2 实例仍然位于单一可用区,因此这依然是一个单点故障,且不能自动扩展。我们需要为 EC2 部分配置自动扩展功能,但首先,如果状态仍然在机器上,我们需要将其移出。

将状态移出 EC2 实例

如果你的应用程序有与其状态相关的文件保存在磁盘上,你需要在应用自动扩展之前将其移除。之前保存在 EC2 实例上的文件必须移除,并由某个服务进行管理。这里有两种选择,如下所示:

  • AWS 弹性文件系统 (aws.amazon.com/efs/):简单来说,这是一个网络文件系统,安装在你的 EC2 机器上,具有几乎无限的空间,你只需为文件所占用的空间付费。

  • AWS S3 (aws.amazon.com/s3/):这是 AWS 市场上的第一个服务,是一个对象存储,旨在提供 99.999999999%的耐久性。

通常情况下,S3 应该是你的首选解决方案,但它并非总是适用,因为使用它需要修改应用软件。因此,在某些情况下,你可能需要一个可以在 EFS 上利用的替代方案。

世界上充满了围绕 S3 设计的软件和插件。例如,WordPress 默认将用户上传的文件保存到磁盘,但通过一个额外的插件,你可以将其保存到 AWS S3,并通过这种方式从 EC2 实例中移除状态。

推送日志

你的实例是一次性的,可以随时被替换或销毁。如果你需要应用特定的日志,你需要使用程序将日志推送到 S3 或 CloudWatch。

配置自动扩展

下面显示的“启动配置”和“自动扩展组”的目的是确保可扩展性和可靠性:

可扩展性和可靠性描述如下:

  • 可扩展性:如果请求量/CPU 增加,系统需要进行扩展并添加实例。同样,如果流量下降,则需要移除不必要的资源。

  • 可靠性:如果某个实例由于任何原因宕机,自动扩展系统会自动用一个新的实例替代它。

你需要快速启动实例以创建镜像,因此可以使用user_data选项,也可以像本章开头的单体配置中一样安装软件程序。然而,这会增加启动新实例所需的时间。

当你需要扩展时,这是因为你需要满足需求的增加,因此需要尽快执行此操作。因此,创建一个包含所有已安装软件和配置文件的镜像,然后将需要在运行时传递的参数或配置文件插入到user_data中是个好主意,如果有的话。

将我们的示例移入自动伸缩中

我们的应用现在已经准备好进行自动伸缩。在这里,状态从 EC2 中移除,仅保存在 RDS 数据库中。我们测试了负载均衡器能否访问它,并检查了它是否能够与数据库进行通信。这就是我们要创建的内容:

准备镜像

我们需要有一个 AMI,以便作为参数传递到启动配置中。为了确保你拥有一个良好的 AMI,建议首先停止机器。当机器停止时,再创建镜像。为此,右键单击镜像部分,然后点击“创建镜像”选项,如下图所示:

选择一个有意义的名称和描述,然后点击“创建镜像”按钮:

根据磁盘大小,镜像将在几分钟内可用。在我们以 8 GB 磁盘为例时,等待时间将较短。

使用向导启动配置部分

要启用自动伸缩过程,需要以下两个对象:

  • 启动配置

  • 自动伸缩组

点击“自动伸缩组”选项,自动向导将开始创建所需的资源:

启动配置是第一步。在这里,选择“我的 AMI”选项并找到在前一步中创建的镜像,如下所示:

现在选择名称。在此步骤中不要修改其他任何内容:

自动伸缩组部分

此时,向导会要求我们提供一些关于自动伸缩部分的详细信息,以便开始配置过程。可以从 1 个实例开始,首先检查一切是否正常工作。

您在自动扩展组中指定的 VPC 和子网可以与之前示例中使用的相同。但是请记住,对于 ALB,必须选择公共子网,而对于 EC2,您可以使用私有或公共子网。在第五章,添加持续集成与持续部署,我们关注安全性,我们将解释为何将 EC2 放置在私有子网中是有益的。

然而,现在使用任何子网都是可以的。重要的是选择多个位于不同可用区(AZ)的子网:

对于安全组,优先选择分配给 EC2 机器的安全组;不要创建新的安全组:

使用您拥有的密钥对来为正常的 EC2 实例进行配置。在理论上,您不需要登录到由自动扩展管理的机器。只有在出现错误且需要调试某些内容时,才需要登录并使用密钥:

扩展策略

这是向导的关键部分,但这是一个稍微有些困难的阶段。扩展策略决定了是否进行扩展(向自动扩展组中添加实例)和缩减(从组中移除实例)的条件。有很多方法可以做到这一点;在这里,我选择了最简单的方法,即通过 CPU 使用率(%):

  • 如果 CPU 使用率低于 70% 且持续超过 5 分钟,添加 1 个实例

  • 如果 CPU 使用率低于 40% 且持续超过 5 分钟,移除 1 个实例

当然,所选择的度量标准和数值取决于您的应用程序,但通过这个示例,您可以大致了解:

必须创建两个警报(每个规则一个),并将其关联到自动扩展组:

这是最终结果:

在下一步中,至少添加标签名称,以便更容易识别由自动扩展组创建的实例:

修改自动扩展组

如果需要修改启动配置,必须创建一个副本并在创建时进行修改,因为不允许直接修改。在自动扩展组中,可以在不重新创建的情况下进行更改。

我们需要修改自动扩展组,因为我们希望每个实例都能注册到与我们的 ALB 关联的目标组:

如果您想手动增加实例数量,只需修改最小大小(Min size)。请记住,所需容量(Desired Capacity)值需要介于最小值和最大值之间:

在实例中,可以看到由自动扩展组创建的新实例:

从负载均衡器中移除手动创建的实例

现在自动扩展功能已经生效,我们可以从负载均衡器中移除用于配置的 EC2 实例,只保留自动生成的实例。如你所见,移除实例时,它不会立即被移除,而是会进入一个短暂的“排空”状态。这样做是为了避免用户体验不佳,并处理仍可能通过该实例连接的情况:

到此为止,自动扩展的配置已经完成,你现在拥有一个满足可扩展性和可靠性要求的应用。

使用微服务和无服务器架构

正如我们在本章测试的那样,将单体应用拆分成多个部分带来了许多优点,但也使整个系统变得更加复杂。

当我们使用微服务和无服务器架构时,这一概念变得更加明显。这是因为,如果正确使用这两种方法,确实可以提高可扩展性、增强可靠性并降低基础设施成本。然而,你总是需要考虑系统会变得更加复杂,构建和管理的难度增加。这导致构建和操作成本上升,尤其是当你的团队第一次构建和管理这种方法的系统时。

以下图像展示了微服务和无服务器架构中负载和成本的概念:

图片来源:medium.freecodecamp.org/serverless-is-cheaper-not-simpler-a10c4fc30e49

总结

扩展是一个长期的过程,有可能不断改进。在本章中,我们完成了第一步,学习了如何利用 AWS 服务将一个单体应用拆分成多个部分。这种方法带来了许多优点,但也使我们的初始基础设施变得更加复杂,这意味着我们需要花费更多时间在配置、修复错误以及学习新服务上。我们已经探索了所有 AWS 工具在可扩展性方面的强大功能和实用性,但有时使用这些工具可能会比较困难,特别是第一次使用时。通过使用 Terraform 模块自动化,我们可以利用模块创建者的知识,立即实现预期的结果。此外,隐藏解决方案的复杂性并不能帮助我们理解背后发生的事情,在修复错误时,这一点尤为重要。因此,本书的某些部分,如自动扩展、ALB 和 SSL 认证,是通过使用 Web 控制台及其向导完成的。

问题

  1. 将单体应用拆分成多层应用总是方便的吗?

  2. 多层架构与微服务/无服务器架构之间有哪些区别?

  3. 从安装在虚拟机中的软件迁移到服务组件时,是否可能会遇到困难?

  4. 负载均衡器能在没有任何干预的情况下管理任何流量高峰吗?

  5. 使用证书管理器而不是经典的 SSL 认证机构能节省费用吗?

  6. 为什么将资源分布在多个可用区(AZ)中很重要?

深入阅读

欲了解更多信息,请阅读以下文章:

第七章:在 AWS 上运行容器

在第六章《扩展你的基础架构》中,我们的架构发生了很大的变化。我们探索了在 AWS 中扩展应用程序的不同方法,但我们遗漏的一个主要技术就是容器。容器是许多大型科技公司软件开发生命周期SDLC)的核心。

到目前为止,我们一直使用个人电脑来开发应用程序。这对于简单的项目,如我们的 Hello World 应用程序,效果很好。然而,当涉及到有许多依赖关系的复杂项目时,情况就不同了。你是否听说过某些功能在开发者的笔记本电脑上运行正常,但在其他组织成员那里却无法运行——甚至更糟的是,在生产环境中无法运行?这些问题中的很多源于环境之间的差异。当我们构建我们的预发布和生产环境时,我们依赖 CloudFormation、Terraform 和 Ansible 来保持这些环境的一致性。不幸的是,我们无法轻松地将其复制到本地开发环境中。

容器解决了这个问题。通过容器,我们可以打包一个应用程序,并包括操作系统、应用程序代码以及中间所有的内容。容器还可以在稍后的阶段提供帮助,当需要突破单体架构时尤为重要。

在本章中,我们将介绍Docker,最受欢迎的容器技术。在简要说明 Docker 是什么以及如何使用其基本功能后,我们将对我们的应用程序进行 Docker 化。这将帮助我们理解作为开发者使用 Docker 的价值。本章将涵盖以下主题:

  • 将我们的 Hello World 应用程序 Docker 化

  • 使用 EC2 容器服务

  • 更新我们的 CI/CD 管道以使用 ECS

本书涵盖了 ECS,但还提供了在 AWS 中使用 Docker 的更多选项。你还可以查看 CoreOS Tectonic(tectonic.com/)、Mesosphere DC/OS(mesosphere.com)或 Docker Datacenter(www.docker.com/products/docker-datacenter)。

技术要求

本章的技术要求如下:

  • Docker

  • Dockerfile

  • EC2 容器注册表ECR

  • 弹性容器服务ECS

  • 应用负载均衡器ALB

  • CodeBuild

  • CodePipeline

本章中使用的代码的 GitHub 链接如下:

将我们的 Hello World 应用程序 Docker 化

Docker 和容器总体来说是非常强大的工具,值得深入探索。通过结合资源隔离功能,包括联合能力文件系统UCF),Docker 允许创建称为容器的包,这些容器包含运行应用程序所需的所有内容。容器像虚拟机一样是自包含的,但它们虚拟化的是操作系统本身,而不是硬件。实际上,这带来了巨大的差异。正如你现在可能已经注意到的那样,启动虚拟机(例如 EC2 实例)需要一定的时间。这是因为为了启动虚拟机,虚拟机监控器(这是创建和运行虚拟机的技术名称)必须模拟启动物理服务器、加载操作系统并通过不同运行级别所涉及的所有动作。此外,虚拟机在磁盘和内存上占据更大的空间。使用 Docker 时,附加的层几乎不可察觉,而且容器的大小可以保持非常小。为了更好地说明这一点,我们将首先安装 Docker,并稍微了解其基本用法。

入门 Docker

在开始使用 Docker 之前,最好先更好地理解 Docker 的概念和架构。首先,我们将讨论 Docker 在 SDLC 中的基本变化。介绍之后,我们将在计算机上安装 Docker,并学习一些使用 Docker 时最常用的命令。

Docker 基础

理解 Docker 工作原理的最好方法是比较使用 Docker 与我们迄今为止所做的事情有什么不同:

上面的图示可以解释如下:

  • 左侧的第一个堆栈表示我们迄今为止所做的工作。使用 EC2 服务,我们选择了提供 AWS Linux 的 AMI,并借助用户数据字段安装了 Ansible 来配置我们的系统。当 Ansible 启动时,它会安装并配置系统,以便稍后 CodeDeploy 可以部署并运行我们的应用程序。

  • 中间的堆栈表示在 EC2 上使用 Docker 的含义。这个过程的开始方式与使用运行 AWS Linux 的 AMI 相同。然而,这次,我们不再依赖 Ansible 和 CodeDeploy,而是直接安装 Docker 服务器应用程序。之后,我们将部署 Docker 容器,这些容器将包含之前由 Ansible 和 CodeDeploy 提供的所有内容。

  • 最终,这种架构的巨大优势在于我们在右侧最后一个堆栈中看到的内容。无论底层技术是什么,只要我们能运行 Docker 服务器,就能运行完全相同的容器。这意味着我们可以轻松地测试将要部署到 EC2 上的内容。类似地,如果一个 EC2 实例上的容器出现问题,我们可以拉取完全相同的容器并在本地运行,以便可能排除问题。

为了实现这一点,Docker 依赖于几个关键概念,如下图所示:

从本质上讲,Docker 运行一个守护进程,该进程加载镜像(描述应用栈的模板,包括操作系统、应用代码以及中间的一切)并在名为容器的自包含目录中运行它们。在 Docker 中工作时,作为开发人员,你的工作主要是通过在现有镜像上叠加新命令来构建新的镜像。镜像存储在外部注册表中,这些注册表可以是公共的,也可以是私有的。最终,所有的交互都通过 RESTful API 完成,通常使用命令行界面。

Docker 实践

为了看到 Docker 的实际操作,我们将从在我们的计算机上安装 Docker 开始。Docker 的安装非常简单;你可以按照 dockr.ly/2iVx6yG 上的说明,安装并启动适用于 Mac、Linux 和 Windows 的 Docker。Docker 提供了两个版本:Docker 社区版 (CE) 和 Docker 企业版 (EE)。在本书中,我们将专注于开源工具,并使用免费的 Docker CE 版本。再次声明,我们将演示的示例是在基于 Linux 的 Centos 7.x 发行版上进行的。如果你也使用相同的操作系统,请按照 docs.docker.com/install/linux/docker-ce/centos/ 上的说明,在本地系统上设置 Docker。当 Docker CE 安装完成后,使用 docker 工具验证安装的 Docker 版本。在编写本书时,18.06 是 Docker 的最新版本,尽管你现在在系统上可能会看到更新的版本:

$ docker –version
Docker version 18.06.1-ce, build e68fc7a 

一旦 Docker 启动并运行,我们可以如下使用它:

  1. 我们要做的第一件事是从注册表中拉取一个镜像。默认情况下,Docker 会指向 Docker Hub (hub.docker.com),这是 Docker 公司官方的 Docker 注册表。为了拉取一个镜像,我们将运行以下命令:
$ docker pull alpine 

我们将使用 latest 默认标签,如下所示:

Using default tag: latest
latest: Pulling from library/alpine
8e3ba11ec2a2: Pull complete
Digest: sha256:7043076348bf5040220df6ad703798fd8593a0918d06d3ce30c6c93be117e430
Status: Downloaded newer image for alpine:latest 
  1. 几秒钟之内,Docker 会从注册表下载名为alpine的镜像,这是一个基于 Alpine Linux 的最小 Docker 镜像,包含完整的软件包索引。这个镜像的大小只有4.41 MB
$ docker images
REPOSITORY  TAG     IMAGE ID      CREATED       SIZE
alpine      latest  11cd0b38bc3c  2 months ago  4.41 MB 

在使用 Docker 时,容器的大小很重要。因此,推荐使用较小的基础镜像,如 Alpine Linux。

  1. 我们现在可以运行我们的容器。为了做到这一点,我们将从以下简单命令开始:
$ docker run alpine echo "Hello World" Hello World 
  1. 表面上看,似乎并没有什么变化,我们得到的输出与没有 Docker 时运行 echo Hello World 得到的输出相同。实际上,幕后发生的事情更有趣;Docker 加载了我们之前拉取的 alpine Linux 镜像,并使用 Alpine 操作系统的 echo 命令打印出 Hello World。最后,由于 echo 命令完成,容器也被终止。

容器也可以以更交互的方式使用,如下所示:

  • 例如,我们可以启动一个 shell 并通过以下命令与其互动:
$ docker run -it alpine /bin/sh

-i 选项表示交互式;它允许我们在容器中输入命令,而 -t 选项分配一个伪 TTY,既能看到我们输入的内容,也能看到命令的输出。

  • 容器也可以通过使用 -d 选项在后台运行,这样可以将我们的容器从终端分离出来:
$ docker run -d alpine sleep 1000 c274537aec04d08c3033f45ab723ba90bcb40240d265851b28f39122199b0600 

该命令返回一个 64 位的容器 ID,表示正在运行 alpine 镜像和 sleep 1000 命令的容器。

  • 我们可以通过使用以下命令跟踪不同的运行容器:
$ docker ps 

运行前述命令的输出如下:

  • 运行中的容器可以通过stop选项停止,后面跟上容器的名称或 ID(根据docker ps命令的输出调整 ID 和名称):
$ docker stop c274537aec04 c274537aec04 

你也可以使用以下命令:

$ docker stop friendly_dijkstra friendly_dijkstra 
  • 停止的容器可以通过start选项重新启动,如下所示:
$ docker start friendly_dijkstra friendly_dijkstra 
  • 最后,容器可以通过使用rm命令删除,但在删除之前请务必先停止容器:
$ docker stop <ID/NAME>
$ docker rm <ID/NAME> 

运行前述命令的输出如下:

这个简要概述应能为我们提供在阅读本章时所需的知识。我们将沿途发现更多命令,但要查看完整的选项列表,你可以使用docker help命令,或查阅 Docker CLI 文档,网址为dockr.ly/2jEF8hj。通过容器运行简单命令有时很有用,但正如我们所知,Docker 的真正优势在于其能够处理任何代码,包括我们的 Web 应用程序。为了实现这一点,我们将使用 Docker 的另一个关键概念:Dockerfile。

创建我们的 Dockerfile

Dockerfile 是文本文件,通常与应用程序一起存放,指示 Docker 如何构建新的 Docker 镜像。通过创建这些文件,你可以告诉 Docker 从哪个 Docker 镜像开始、要复制到容器文件系统中的内容、要公开的网络端口等等。你可以在dockr.ly/2jmoZMw找到 Dockerfile 的完整文档。我们将为我们的 Hello World 应用程序创建一个 Dockerfile,位于我们在 GitHub 仓库中创建的helloworld项目的根目录,使用以下命令:

$ cd helloworld
$ touch Dockerfile 

Dockerfile 的第一条指令始终是FROM指令。这告诉 Docker 从哪个 Docker 镜像开始。我们可以使用 Alpine 镜像,正如我们所做的那样,但我们也可以通过使用一个不仅仅是操作系统的镜像来节省一些时间。通过 Docker Hub,Docker 的官方注册表,Docker 提供了许多经过精心策划的 Docker 仓库集,这些仓库被称为官方。我们知道,为了运行我们的应用程序,我们需要 Node.js 和npm。我们可以使用 Docker CLI 查找官方的node镜像。为此,我们将使用docker search命令,并仅筛选官方镜像:

$ docker search --filter=is-official=true node
NAME    DESCRIPTION                                   STARS  OFFICIAL     
           AUTOMATED
node    Node.js is a JavaScript-based platform for s… 6123    [OK] 

或者,我们也可以使用浏览器进行搜索。结果,我们将看到相同的镜像,hub.docker.com/_/node/。正如我们所见,以下截图有多个版本:

Docker 镜像总是由名称和标签组成,使用语法name:tag。如果省略标签,Docker 将默认使用latest标签。从前面的docker pull命令中,我们可以看到输出显示Using default tag: latest。在创建 Dockerfile 时,最佳实践是使用一个不会随着时间改变的显式标签(不同于latest标签)。

如果你正在尝试迁移一个当前运行在 AWS Linux 上的应用程序,并基于该操作系统做出一些假设,你可能想要使用官方的 AWS Docker 镜像。你可以在amzn.to/2jnmklF上了解更多信息。

在文件的第一行,我们将添加如下内容:

FROM node:carbon

这将告诉 Docker 我们想要使用特定版本的node镜像。这意味着我们不需要安装nodenpm。因为我们已经有了应用程序所需的操作系统和运行时二进制文件,所以我们可以开始考虑将我们的应用程序添加到这个镜像中。首先,我们需要在node:carbon镜像的文件系统上创建一个目录,用来存放我们的代码。我们可以使用RUN指令来完成,具体如下:

RUN mkdir -p /usr/local/helloworld/

现在,我们想要将应用程序文件复制到镜像中。我们将使用COPY指令来实现:

COPY helloworld.js package.json /usr/local/helloworld/

确保你将helloworld.jspackage.json文件复制到你本地开发 Dockerfile 的/helloworld项目目录中。这些文件位于github.com/yogeshraheja/helloworld/blob/master/helloworld.jsgithub.com/yogeshraheja/helloworld/blob/master/package.json

我们将使用WORKDIR指令将我们的新工作目录设置为helloworld目录:

 WORKDIR /usr/local/helloworld/

现在我们可以运行npm install命令来下载并安装我们的依赖项。因为我们不会使用这个容器来测试我们的代码,我们可以只安装生产环境所需的npm包,如下所示:

RUN npm install --production

我们的应用程序使用3000端口。我们需要让这个端口对主机可访问。为此,我们将使用EXPOSE指令:

EXPOSE 3000

最后,我们可以启动我们的应用程序。为此,我们将使用ENTRYPOINT指令:

ENTRYPOINT [ "node", "helloworld.js" ]

我们现在可以保存文件。它应该看起来像github.com/yogeshraheja/helloworld/blob/master/Dockerfile模板那样。我们现在可以构建我们的新镜像。

回到终端,我们将再次使用docker命令,但这次使用build参数。我们还将使用-t选项为我们的镜像提供名称helloworld,并在后面加上一个(.)点,表示 Dockerfile 的位置:

$ docker build -t helloworld .
Sending build context to Docker daemon 4.608kB
Step 1/7 : FROM node:carbon
carbon: Pulling from library/node
f189db1b88b3: Pull complete
3d06cf2f1b5e: Pull complete
687ebdda822c: Pull complete
99119ca3f34e: Pull complete
e771d6006054: Pull complete
b0cc28d0be2c: Pull complete
9bbe77ca0944: Pull complete
75f7d70e2d07: Pull complete
Digest: sha256:3422df4f7532b26b55275ad7b6dc17ec35f77192b04ce22e62e43541f3d28eb3
Status: Downloaded newer image for node:carbon
 ---> 8198006b2b57
Step 2/7 : RUN mkdir -p /usr/local/helloworld/
 ---> Running in 2c727397cb3e
Removing intermediate container 2c727397cb3e
 ---> dfce290bb326
Step 3/7 : COPY helloworld.js package.json /usr/local/helloworld/
 ---> ad79109b5462
Step 4/7 : WORKDIR /usr/local/helloworld/
 ---> Running in e712a394acd7
Removing intermediate container e712a394acd7
 ---> b80e558dff23
Step 5/7 : RUN npm install --production
 ---> Running in 53c81e3c707a
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN helloworld@1.0.0 No description

up to date in 0.089s
Removing intermediate container 53c81e3c707a
 ---> 66c0acc080f2
Step 6/7 : EXPOSE 3000
 ---> Running in 8ceba9409a63
Removing intermediate container 8ceba9409a63
 ---> 1902103f865c
Step 7/7 : ENTRYPOINT [ "node", "helloworld.js" ]
 ---> Running in f73783248c5f 
Removing intermediate container f73783248c5f
 ---> 4a6cb81d088d
Successfully built 4a6cb81d088d
Successfully tagged helloworld:latest 

如你所见,每个命令都会生成一个新的中间容器,并带有该步骤所触发的更改。

现在我们可以运行我们新创建的镜像,以以下命令创建一个容器:

$ docker run -p 3000:3000 -d helloworld e47e4130e545e1b2d5eb2b8abb3a228dada2b194230f96f462a5612af521ddc5 

在这里,我们在命令中添加了-p选项,将容器的暴露端口映射到主机的端口。有几种方法可以验证我们的容器是否正常工作。我们可以首先查看容器生成的日志(将容器 ID 替换为之前命令的输出):

$ docker logs e47e4130e545e1b2d5eb2b8abb3a228dada2b194230f96f462a5612af521ddc5 
Server running 

我们还可以使用docker ps命令查看我们容器的状态:

$ docker ps 

上述命令的输出如下:

当然,我们还可以简单地使用curl命令来测试应用:

$ curl localhost:3000
Hello World 

另外,如果你的主机有公共 IP,你甚至可以通过浏览器使用<ip:exposedport>来验证输出,在我的例子中是54.205.200.149:3000

最后,使用docker kill命令和容器 ID 来停止容器:

$ docker kill e47e4130e545
e47e4130e545

由于我们的镜像运行正常,我们可以将代码提交到 GitHub:

$ git add Dockerfile
$ git commit -m "Adding Dockerfile"
$ git push 

此外,你现在可以在 Docker Hub 上创建一个帐户(免费)并上传这个新镜像。如果你想尝试,你可以按照dockr.ly/2ki6DQV上的说明进行操作。

能够轻松分享容器,在协作项目中带来很大的不同。你无需分享代码并要求别人编译或构建包,而是可以直接分享 Docker 镜像。例如,可以通过运行以下命令来实现:

docker pull yogeshraheja/helloworld 

运行上述命令的输出如下:

你可以体验 Hello World 应用,正如我所看到的一样,无论你的底层架构是什么。通过这种新的应用运行方式,Docker 成为了一个非常强大的工作分享或项目协作解决方案。然而,Docker 的优势不仅限于工作协作。正如我们即将看到的,使用容器进行生产部署也是一个非常有趣的选择。为了便于实现这些解决方案,AWS 创建了 EC2 容器服务。我们将使用它来部署我们新创建的helloworld镜像。

使用 EC2 容器服务

我们刚刚讲解了如何为我们的应用创建一个 Docker 镜像。在这里,我们看到使用 Docker 启动容器是多么简单和快速。这与仅使用虚拟机技术(如 EC2)相比,带来了非常有变革性的体验。我们至今没有明确提到的一种可能性是,你可以使用相同的镜像启动多个容器。例如,我们可以启动五次helloworld容器,并使用以下命令绑定五个不同的端口(根据你构建的镜像 ID 调整 ID。如果需要,可以运行 Docker images 来查找镜像 ID):

$ for p in {3001..3005}; do docker run -d -p ${p}:3000 4a6cb81d088d; done  

我们可以使用pscurl命令验证一切是否正常:

$ docker ps $ curl localhost:3005 

运行上述命令的输出如下:

清理容器:

我们可以通过停止并删除所有容器来清理一切,使用以下两个便捷的一行命令:

  • $ docker stop $(docker ps -a -q)

  • $ docker system prune

执行前面命令的输出结果如下:

这种几乎没有开销或延迟的单主机多容器启动能力,使 Docker 成为生产环境中的理想选择。此外,越来越多的公司决定通过将每个业务功能拆分为独立服务,将面向服务的架构方法提升到一个全新的水平。这通常被称为 微服务 方法。Docker 非常适合微服务及其管理。这是因为它提供了一个与语言无关的平台(您可以在容器中启动任何语言编写的应用程序),能够轻松地进行横向和纵向扩展,并且围绕部署有一个共同的故事——我们部署的是容器,而不是多种服务。我们将使用 基础设施即代码IaC)最佳实践来实现我们的容器架构,并通过 Troposphere 中介使用 CloudFormation。我们将要查看的第一个服务是 AWS 的 ECR。

创建 ECR 仓库来管理我们的 Docker 镜像

在本章的第一部分,我们使用了 Docker Hub 公共注册表。AWS 提供了一个类似的服务,叫做 ECR。这使您能够将镜像保存在一个名为 repository 的私有注册表中。ECR 完全兼容 Docker CLI,并且与其他 ECS 服务深度集成。我们将使用它来存储我们的 helloworld 镜像。

如前所述,我们将主要依赖 CloudFormation 来进行我们的更改。与之前看到的不同,由于其性质,我们将要构建的 ECS 基础设施需要非常模块化。这是因为在实际操作中,我们希望将其中一些组件与其他服务共享。因此,我们将创建多个模板,并将它们相互链接。实现这一点的一个好方法是依赖 CloudFormation 的导出功能,这使我们能够进行跨堆栈引用。

导出的一个附加优势是其故障保护机制。如果另一个堆栈引用了导出的输出,您就无法删除或编辑该堆栈。

为了生成我们的模板,我们将创建一个新的 Troposphere 脚本。为此,进入 EffectiveDevOpsTemplates 仓库,并创建一个名为 ecr-repository-cf-template.py 的新脚本。

我们将首先导入多个模块,包括前面提到的 Export 模块和 ecr 模块,以便创建我们的仓库。我们还将像前几章一样创建我们的模板变量 t

"""Generating CloudFormation template."""

from troposphere import ( 
Export,
Join, 
Output,
Parameter, 
Ref, 
Template
)
from troposphere.ecr import Repository 
t = Template()

由于我们将在本章中创建多个 CloudFormation 模板,因此我们将添加描述,以便在 AWS 控制台中查看这些模板时更容易理解每个模板的功能:

t.add_description("Effective DevOps in AWS: ECR Repository") 

我们将为仓库名称创建一个参数,这样我们就能将此 CloudFormation 模板用于每个我们创建的仓库:

t.add_parameter(Parameter( 
       "RepoName", 
        Type="String",
        Description="Name of the ECR repository to create"
))

现在我们可以按如下方式创建我们的仓库:

t.add_resource(Repository( 
        "Repository", 
         RepositoryName=Ref("RepoName")
))

我们这里的代码保持简单,并没有强制执行任何特定的权限。如果你需要限制谁可以访问你的仓库并查看更复杂的配置,可以参考 AWS 文档,特别是 amzn.to/2j7hA2P。最后,我们将输出我们创建的仓库名称,并通过模板变量 t 导出其值:

t.add_output(Output(
    "Repository",
    Description="ECR repository",
    Value=Ref("RepoName"),
    Export=Export(Join("-", [Ref("RepoName"), "repo"])),
))
print(t.to_json())

现在我们可以保存我们的脚本。它应该像这样:github.com/yogeshraheja/EffectiveDevOpsTemplates/blob/master/ecr-repository-cf-template.py。接下来,我们将生成 CloudFormation 模板并按如下方式创建我们的堆栈:

$ python ecr-repository-cf-template.py > ecr-repository-cf.template
$ aws cloudformation create-stack \
 --stack-name helloworld-ecr \
 --capabilities CAPABILITY_IAM \
 --template-body file://ecr-repository-cf.template \
 --parameters \ ParameterKey=RepoName,ParameterValue=helloworld 

几分钟后,我们的堆栈将会创建完成。我们可以验证仓库是否正确创建,方法如下:

$ aws ecr describe-repositories
{
 "repositories": [
 {
 "registryId": "094507990803",
 "repositoryName": "helloworld",
 "repositoryArn": "arn:aws:ecr:us-east- 
             1:094507990803:repository/helloworld",
 "createdAt": 1536345671.0,
 "repositoryUri": "094507990803.dkr.ecr.us-east-
             1.amazonaws.com/helloworld"
 }
 ]
} 

我们可以通过以下命令查看导出的输出:

$ aws cloudformation list-exports
{
 "Exports": [
 {
 "ExportingStackId": "arn:aws:cloudformation:us-east-
             1:094507990803:stack/helloworld-ecr/94d9ed70-b2cd-11e8-
             b767-50d501eed2b3",
 "Value": "helloworld",
 "Name": "helloworld-repo"
 }
 ]
} 

现在,我们的仓库可以用来存储我们的 helloworld 镜像。我们将使用 Docker CLI 来完成这一步。该过程的第一步是登录到 ecr 服务。你可以通过以下简洁的一行命令来完成:

$ eval "$(aws ecr get-login --region us-east-1 --no-include-email )" 

执行前述命令的输出结果如下所示:

在我们 helloworld 目录下,也就是 Dockerfile 所在的目录,我们将按如下方式标记我们的镜像:

$ cd helloworld 

使用 latest 标签来指定镜像的最新版本是一种常见的做法。此外,您需要根据 aws ecr describe-repositories 输出的内容调整以下命令(在这里我们假设你已经构建了镜像):

$ docker tag helloworld:latest 094507990803.dkr.ecr.us-east-1.amazonaws.com/helloworld:latest 

现在,我们可以按照以下方式将镜像推送到我们的注册表:

$ docker push 094507990803.dkr.ecr.us-east-1.amazonaws.com/helloworld:latest
The push refers to repository [094507990803.dkr.ecr.us-east-1.amazonaws.com/helloworld]
c7f21f8d59de: Pushed
3c36cf19a914: Pushed
8faa1d9821d6: Pushed
be0fb77bfb1f: Pushed
63c810287aa2: Pushed
2793dc0607dd: Pushed
74800c25aa8c: Pushed
ba504a540674: Pushed
81101ce649d5: Pushed
daf45b2cad9a: Pushed
8c466bf4ca6f: Pushed
latest: digest: sha256:95906ec13adf9894e4611cd37c8a06569964af0adbb035fcafa6020994675161 size: 2628 

我们可以看到每一层镜像是如何并行推送到我们的注册表中的。一旦操作完成,我们可以验证新的镜像是否已经出现在注册表中,方法如下:

$ aws ecr describe-images --repository-name helloworld
{
 "imageDetails": [
 {
 "imageSizeInBytes": 265821145,
 "imageDigest": 
"sha256:95906ec13adf9894e4611cd37c8a06569964af0adbb035fcafa6020994675161",
 "imageTags": [
 "latest"
 ],
 "registryId": "094507990803",
 "repositoryName": "helloworld",
 "imagePushedAt": 1536346218.0
 }
 ]
} 

此时,我们的镜像已经可以供其他基础设施使用。接下来,我们将进入流程的下一步,即创建 ECS 集群。

创建 ECS 集群

创建 ECS 集群的过程与我们在 第六章 扩展基础设施 中创建自动扩展组以运行 Hello World 应用程序时非常相似。主要的区别是,多了一个抽象层。ECS 将运行多个被称为 任务(task)的服务。

这些任务可能会多次存在,以便处理流量:

为了实现这一目标,ECS 服务提供了一个编排层。该编排层负责管理容器的生命周期,包括升级或降级、以及水平扩展或缩减容器。编排层还会在集群的所有实例上,优化地分配每个服务的所有容器。最后,它还提供了一种发现机制,与其他服务如 ALB 和 ELB 进行交互,用于注册和注销容器。

任务放置策略:

默认情况下,整个编排系统由 AWS 管理。然而,您也可以通过创建任务放置策略来对其进行自定义。这将允许您配置编排,以优化实例数量、负载分配、添加约束,并确保某些任务在相同的实例上启动。

我们将创建一个新的脚本来生成我们的 ECS 集群。文件名将是 ecs-cluster-cf-template.py。这个模板几乎与我们在第六章中为自动扩展组创建的模板完全相同,扩展您的基础设施

"""Generating CloudFormation template."""

from ipaddress import ip_network from ipify import get_ip
from troposphere import (
    Base64,
    Export, 
    Join, 
    Output, 
    Parameter, 
    Ref,
    Sub, 
    Template, 
    ec2
)

from troposphere.autoscaling import ( 
    AutoScalingGroup, 
    LaunchConfiguration, 
    ScalingPolicy
)

from troposphere.cloudwatch import ( 
    Alarm,
    MetricDimension
)
from troposphere.ecs import Cluster
from troposphere.iam import (
    InstanceProfile, 
    Role
)

唯一需要导入的是来自 ECS 模块的集群模块。就像我们在第六章中所做的那样,扩展您的基础设施,我们将提取我们的 IP 地址,以便稍后用于 SSH 安全组,创建我们的模板变量,并为堆栈添加描述:

PublicCidrIp = str(ip_network(get_ip()))
t = Template()
t.add_description("Effective DevOps in AWS: ECS Cluster")

我们现在将继续添加我们的参数,这些参数与在第六章中使用的参数相同,扩展您的基础设施。这包括 SSH 密钥对、VPC ID 及其子网:

t.add_parameter(Parameter(
    "KeyPair",
    Description="Name of an existing EC2 KeyPair to SSH",
    Type="AWS::EC2::KeyPair::KeyName",
    ConstraintDescription="must be the name of an existing EC2   
    KeyPair.",
))

t.add_parameter(Parameter(
    "VpcId",
    Type="AWS::EC2::VPC::Id",
    Description="VPC"
))

t.add_parameter(Parameter(
    "PublicSubnet",
    Description="PublicSubnet",
    Type="List<AWS::EC2::Subnet::Id>",
    ConstraintDescription="PublicSubnet"
))

接下来,我们将创建我们的安全组资源:

t.add_resource(ec2.SecurityGroup(
    "SecurityGroup",
    GroupDescription="Allow SSH and private network access",
    SecurityGroupIngress=[
        ec2.SecurityGroupRule(
            IpProtocol="tcp",
            FromPort=0,
            ToPort=65535,
            CidrIp="172.16.0.0/12",
        ),
        ec2.SecurityGroupRule(
            IpProtocol="tcp",
            FromPort="22",
            ToPort="22",
            CidrIp=PublicCidrIp,
        ),
    ],
    VpcId=Ref("VpcId")
))

这里有一个重要的区别。在第六章中,扩展您的基础设施,我们打开了端口 3000,因为那是我们的应用程序使用的端口。这里,我们将所有端口都开放到 CIDR 1 72.16.0.0/12,这是我们内部网络的私有 IP 地址空间。这将使我们的 ECS 集群能够在相同的主机上运行多个 helloworld 容器,绑定不同的端口。

我们现在将创建我们的集群资源。只需使用以下命令即可完成:

t.add_resource(Cluster(
    'ECSCluster',
))

接下来,我们将专注于配置集群的实例,从它们的 IAM 角色开始。总体来说,这是 ECS 中创建的更复杂的资源之一,因为集群需要与其他 AWS 服务进行多次交互。我们可以为其创建一个完整的自定义策略,或者导入 AWS 创建的策略,如下所示:

t.add_resource(Role(
    'EcsClusterRole',
    ManagedPolicyArns=[
        'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM',
        'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly',
        'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role',
        'arn:aws:iam::aws:policy/CloudWatchFullAccess'
    ],
    AssumeRolePolicyDocument={
        'Version': '2012-10-17',
        'Statement': [{
            'Action': 'sts:AssumeRole',
            'Principal': {'Service': 'ec2.amazonaws.com'},
            'Effect': 'Allow',
        }]
    }
))

我们现在可以将角色与实例配置文件关联起来,如下所示:

t.add_resource(InstanceProfile(
    'EC2InstanceProfile',
    Roles=[Ref('EcsClusterRole')],
))

下一步是创建我们的启动配置。以下代码片段展示了它的样子:

t.add_resource(LaunchConfiguration(
    'ContainerInstances',
     UserData=Base64(Join('', [
        "#!/bin/bash -xe\n",
        "echo ECS_CLUSTER=",
        Ref('ECSCluster'),
        " >> /etc/ecs/ecs.config\n",
        "yum install -y aws-cfn-bootstrap\n",
        "/opt/aws/bin/cfn-signal -e $? ",
        " --stack ",
        Ref('AWS::StackName'),
        " --resource ECSAutoScalingGroup ",
        " --region ",
        Ref('AWS::Region'),
        "\n"])),
    ImageId='ami-04351e12',
    KeyName=Ref("KeyPair"),
    SecurityGroups=[Ref("SecurityGroup")],
    IamInstanceProfile=Ref('EC2InstanceProfile'),
    InstanceType='t2.micro',
    AssociatePublicIpAddress='true',
))

在此示例中,我们不像之前那样安装 Ansible。相反,我们使用一个经过 ECS 优化的 AMI(您可以在amzn.to/2jX0xVu上查看更多信息),它允许我们使用UserData字段来配置 ECS 服务,并启动它。现在我们有了启动配置,可以创建 Auto Scaling Group 资源。

在使用 ECS 时,需要在两个层级进行扩展:

  • 容器级别,因为如果流量激增,我们需要运行更多的容器来处理指定的服务

  • 底层基础设施级别

通过任务定义,容器设置了对 CPU 和内存的需求。例如,它们可能需要 1024 CPU 单元,表示一个核心,以及 256 内存单元,表示 256 MB 的 RAM。如果 ECS 实例的某一约束接近满载,ECS Auto Scaling Group 需要增加更多实例:

在实现方面,该过程与我们在第六章中做的扩展基础设施非常相似。在这里,我们首先创建 Auto Scaling Group 资源,如下所示:

t.add_resource(AutoScalingGroup(
    'ECSAutoScalingGroup',
    DesiredCapacity='1',
    MinSize='1',
    MaxSize='5',
    VPCZoneIdentifier=Ref("PublicSubnet"),
    LaunchConfigurationName=Ref('ContainerInstances'),
))

接下来,我们将创建扩展策略和告警,以监控 CPU 和内存预留指标。为了实现这一点,我们将利用 Python 生成我们的堆栈,并创建如下的循环:

states = {
    "High": {
        "threshold": "75",
        "alarmPrefix": "ScaleUpPolicyFor",
        "operator": "GreaterThanThreshold",
        "adjustment": "1"
    },
    "Low": {
        "threshold": "30",
        "alarmPrefix": "ScaleDownPolicyFor",
        "operator": "LessThanThreshold",
        "adjustment": "-1"
    }
}

for reservation in {"CPU", "Memory"}:
    for state, value in states.iteritems():
        t.add_resource(Alarm(
            "{}ReservationToo{}".format(reservation, state),
            AlarmDescription="Alarm if {} reservation too {}".format(
                reservation,
                state),
            Namespace="AWS/ECS",
            MetricName="{}Reservation".format(reservation),
            Dimensions=[
                MetricDimension(
                    Name="ClusterName",
                    Value=Ref("ECSCluster")
                ),
            ],
            Statistic="Average",
            Period="60",
            EvaluationPeriods="1",
            Threshold=value['threshold'],
            ComparisonOperator=value['operator'],
            AlarmActions=[
                Ref("{}{}".format(value['alarmPrefix'], reservation))]
        ))
        t.add_resource(ScalingPolicy(
            "{}{}".format(value['alarmPrefix'], reservation),
            ScalingAdjustment=value['adjustment'],
            AutoScalingGroupName=Ref("ECSAutoScalingGroup"),
            AdjustmentType="ChangeInCapacity",
        ))

最后,我们将提供一些资源信息,特别是堆栈 ID、VPC ID 和公共子网:

t.add_output(Output(
    "Cluster",
    Description="ECS Cluster Name",
    Value=Ref("ECSCluster"),
    Export=Export(Sub("${AWS::StackName}-id")),
))

t.add_output(Output(
    "VpcId",
    Description="VpcId",
    Value=Ref("VpcId"),
    Export=Export(Sub("${AWS::StackName}-vpc-id")),
))

t.add_output(Output(
    "PublicSubnet",
    Description="PublicSubnet",
    Value=Join(',', Ref("PublicSubnet")),
    Export=Export(Sub("${AWS::StackName}-public-subnets")),
))

print(t.to_json())

CloudFormation 提供了许多伪参数,如AWS::StackName。在本章中,我们将依赖它来使我们的模板足够通用,以便在不同环境和服务中使用。在前面的代码中,我们为我们的helloworld容器创建了一个 ECR 仓库。该名称是通过堆栈创建命令生成的。如果需要,我们可以重用相同的模板为另一个容器创建另一个仓库。

脚本现在已经完成,应该与以下脚本相似:github.com/yogeshraheja/EffectiveDevOpsTemplates/blob/master/ecs-cluster-cf-template.py

如前所述,我们现在可以提交脚本并通过首先生成模板来创建我们的堆栈,如下所示:

$ git add ecs-cluster-cf-template.py
$ git commit -m "Adding Troposphere script to generate an ECS cluster"
$ git push
$ python ecs-cluster-cf-template.py > ecs-cluster-cf.template 

要创建堆栈,我们需要三个参数:密钥对、VPC ID 和子网。在前几章中,我们使用 Web 界面来创建这些堆栈。这里,我们将介绍如何使用 CLI 获取这些信息。

要获取 VPC ID 和子网 ID,我们可以使用以下命令:

$ aws ec2 describe-vpcs --query 'Vpcs[].VpcId' 
[
 "vpc-4cddce2a"
]
$ aws ec2 describe-subnets --query 'Subnets[].SubnetId' 
[
 "subnet-e67190bc",
 "subnet-658b6149",
 "subnet-d890d3e4",
 "subnet-6fdd7927",
 "subnet-4c99c229",
 "subnet-b03baebc"
] 

我们现在可以通过结合前面的输出创建我们的栈。由于 ECS 集群可以运行各种容器和多个应用程序及服务,我们将为每个环境创建一个 ECS 集群,从 staging 开始。为了区分每个环境,我们将依赖栈的名称。因此,重要的是将你的栈命名为 staging-cluster,如这里所示:

$ aws cloudformation create-stack \
    --stack-name staging-cluster \
    --capabilities CAPABILITY_IAM \
    --template-body file://ecs-cluster-cf.template \
    --parameters \             
    ParameterKey=KeyPair,ParameterValue=EffectiveDevOpsAWS \     
    ParameterKey=VpcId,ParameterValue=vpc-4cddce2a \
    ParameterKey=PublicSubnet,ParameterValue=subnet-e67190bc\\,subnet-
    658b6149\\,subnet-d890d3e4\\,subnet-6fdd7927\\,subnet-
    4c99c229\\,subnet-b03baebc
{
    "StackId": "arn:aws:cloudformation:us-east-   
    1:094507990803:stack/staging-cluster/581e30d0-b2d2-11e8-b48f-
    503acac41e99"
} 

我们现在将添加一个负载均衡器。在上一章中,我们为我们的自动扩展组使用了 ELB。之后,我们也提到了 ALB 服务的存在。这次,我们将创建一个 ALB 实例来代理我们的应用流量。

创建 ALB

如前所述,ECS 提供了一个协调器,用于管理容器在自动扩展组中的分配。它还跟踪每个容器使用的端口,并与 ALB 集成,以便我们的负载均衡器能够正确地将流量路由到运行给定服务的所有容器。ECS 支持 ELB 和 ALB 服务,但 ALB 在与容器协作时提供了更多的灵活性。我们将演示如何通过 Troposphere 使用 CloudFormation 创建 ALB。

我们将首先创建一个新文件,并将其命名为 helloworld-ecs-alb-cf-template.py。然后我们将像往常一样添加导入,并创建我们的模板变量,并添加描述,如下所示:

"""Generating CloudFormation template."""

from troposphere import elasticloadbalancingv2 as elb

from troposphere import (
    Export,
    GetAtt,
    ImportValue,
    Join,
    Output,
    Ref,
    Select,
    Split,
    Sub,
    Template,
    ec2
)

t = Template()

t.add_description("Effective DevOps in AWS: ALB for the ECS Cluster")

现在我们要创建安全组。这里没有惊喜;我们将 TCP/3000 开放给全世界,就像在第六章《扩展你的基础设施》中,使用 ELB 时所做的那样:

t.add_resource(ec2.SecurityGroup(
    "LoadBalancerSecurityGroup",
    GroupDescription="Web load balancer security group.",
    VpcId=ImportValue(
        Join(
            "-",
            [Select(0, Split("-", Ref("AWS::StackName"))),
                "cluster-vpc-id"]
        )
    ),
    SecurityGroupIngress=[
        ec2.SecurityGroupRule(
            IpProtocol="tcp",
            FromPort="3000",
            ToPort="3000",
            CidrIp="0.0.0.0/0",
        ),
    ],
))

与我们之前所做的主要不同之处在于,我们不再从一个参数部分开始并再次要求提供 VPC ID 和公有子网,而是利用我们之前导出的值。当我们启动这个栈时,我们将其命名为 staging-albImportValue 参数中的代码块执行如下操作:

  1. 首先,我们获取栈的名称。我们将使用 staging-alb 作为该栈的名称。

  2. Split 函数将栈名称按字符 - 拆分,意味着我们最终得到 [staging, alb]。

  3. Select 函数获取列表中的第一个元素:staging。

  4. Join 函数将该元素与字符串 cluster-vpc-id 连接起来。最后,我们得到 Import("staging-cluster-vpc-id"),这是我们在创建 ECS 集群时定义并导出 VPC ID 的键的名称:

现在,我们将创建我们的 ALB。ALB 比 ELB 更灵活、功能更强大,因此在配置时需要更多的工作。ALB 通过三种不同的资源中介工作。第一个是 ALB 资源,处理传入的连接。在另一端,我们可以找到目标组,这些是 ECS 集群用于注册到这些 ALB 的资源。最后,为了将两者连接起来,我们需要监听器资源。我们将首先定义我们的负载均衡器资源,如下所示:

t.add_resource(elb.LoadBalancer(
    "LoadBalancer",
    Scheme="internet-facing",
    Subnets=Split(
        ',',
        ImportValue(
            Join("-",
                 [Select(0, Split("-", Ref("AWS::StackName"))),
                  "cluster-public-subnets"]
                 )
        )
    ),
    SecurityGroups=[Ref("LoadBalancerSecurityGroup")],
))

我们使用与之前为 VPC ID 导入子网时非常相似的调用来导入我们的子网。

现在,我们将创建我们的目标组并配置健康检查,如下所示:

t.add_resource(elb.TargetGroup(
    "TargetGroup",
    DependsOn='LoadBalancer',
    HealthCheckIntervalSeconds="20",
    HealthCheckProtocol="HTTP",
    HealthCheckTimeoutSeconds="15",
    HealthyThresholdCount="5",
    Matcher=elb.Matcher(
        HttpCode="200"),
    Port=3000,
    Protocol="HTTP",
    UnhealthyThresholdCount="3",
    VpcId=ImportValue(
        Join(
            "-",
            [Select(0, Split("-", Ref("AWS::StackName"))),
                "cluster-vpc-id"]
        )
    ),
))

最后,我们将添加监听器,将目标组连接到负载均衡器:

t.add_resource(elb.Listener(
    "Listener",
    Port="3000",
    Protocol="HTTP",
    LoadBalancerArn=Ref("LoadBalancer"),
    DefaultActions=[elb.Action(
        Type="forward",
        TargetGroupArn=Ref("TargetGroup")
    )]
))

最后,我们希望创建两个输出。第一个输出是目标组。我们将导出其值,以便我们的应用程序可以注册到该组。第二个输出是 ALB 的 DNS 记录。这将成为我们应用程序的入口点:

t.add_output(Output(
    "TargetGroup",
    Description="TargetGroup",
    Value=Ref("TargetGroup"),
    Export=Export(Sub("${AWS::StackName}-target-group")),
))

t.add_output(Output(
    "URL",
    Description="Helloworld URL",
    Value=Join("", ["http://", GetAtt("LoadBalancer", "DNSName"), ":3000"])
))

print(t.to_json())

文件现在已经准备好,应该看起来像这个文件:github.com/yogeshraheja/EffectiveDevOpsTemplates/blob/master/helloworld-ecs-alb-cf-template.py。我们现在可以生成我们的模板并创建堆栈,如下所示:

$ git add helloworld-ecs-alb-cf-template.py
$ git commit -m "Adding a Load balancer template for our helloworld application on ECS"
$ git push
$ python helloworld-ecs-alb-cf-template.py > helloworld-ecs-alb-cf.template
$ aws cloudformation create-stack \
 --stack-name staging-alb \
 --capabilities CAPABILITY_IAM \
 --template-body file://helloworld-ecs-alb-cf.template
 {
 "StackId": "arn:aws:cloudformation:us-east-        
     1:094507990803:stack/staging-alb/4929fee0-b2d4-11e8-825f-
     50fa5f2588d2"
} 

如前所述,重要的是将堆栈命名为staging-alb,并且第一个词用来导入 VPC ID 和子网。我们需要的最后一个堆栈是创建我们的容器服务。

创建我们的 ECS Hello World 服务

我们有一个 ECS 集群和一个负载均衡器,准备好在一侧处理流量,另一侧则有一个 ECR 仓库,包含我们应用程序的镜像。现在,我们需要将这两者连接起来。这是通过创建一个 ECS 服务资源来完成的。我们将创建一个名为helloworld-ecs-service-cf-template.py的新文件,并像往常一样从导入、模板变量创建和模板描述开始:

"""Generating CloudFormation template."""

from troposphere.ecs import (
    TaskDefinition,
    ContainerDefinition
)
from troposphere import ecs
from awacs.aws import (
    Allow,
    Statement,
    Principal,
    Policy
)
from troposphere.iam import Role

from troposphere import (
    Parameter,
    Ref,
    Template,
    Join,
    ImportValue,
    Select,
    Split,
)

from awacs.sts import AssumeRole

t = Template()

t.add_description("Effective DevOps in AWS: ECS service - Helloworld")

我们的模板将需要一个参数,即我们想要部署的镜像标签。我们的仓库目前只有一个标记为 latest 的镜像,但在接下来的章节中,我们将更新我们的部署管道,并自动化将我们的服务部署到 ECS:

t.add_parameter(Parameter(
    "Tag",
    Type="String",
    Default="latest",
    Description="Tag to deploy"
))

在 ECS 中,应用程序由其任务定义来定义。这里是我们声明使用哪个仓库来获取我们的镜像、应用程序需要多少 CPU 和内存,以及所有其他系统属性(如端口映射、环境变量、挂载点等)的位置。我们将保持任务定义的最小化;为了选择合适的镜像,我们将利用ImportValue函数(我们之前导出了仓库名称)并结合Join函数来构建仓库 URL。我们将需要 32 MB 的 RAM 和四分之一的 CPU 核心来运行我们的应用程序。最后,我们将指定端口3000需要映射到系统上:

t.add_resource(TaskDefinition(
    "task",
    ContainerDefinitions=[
        ContainerDefinition(
            Image=Join("", [
                Ref("AWS::AccountId"),
                ".dkr.ecr.",
                Ref("AWS::Region"),
                ".amazonaws.com",
                "/",
                ImportValue("helloworld-repo"),
                ":",
                Ref("Tag")]),
            Memory=32,
            Cpu=256,
            Name="helloworld",
            PortMappings=[ecs.PortMapping(
                ContainerPort=3000)]
        )
    ],
))

对于大多数 AWS 托管服务,ECS 服务需要通过角色中介提供的特定权限。我们将创建该角色,并使用默认策略来配置 ECS 服务角色,步骤如下:

t.add_resource(Role(
    "ServiceRole",
    AssumeRolePolicyDocument=Policy(
        Statement=[
            Statement(
                Effect=Allow,
                Action=[AssumeRole],
                Principal=Principal("Service", ["ecs.amazonaws.com"])
            )
        ]
    ),
    Path="/",
    ManagedPolicyArns=[
        'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole']
))

我们将通过添加 ECS 服务资源来完成模板的创建,这将任务定义、ECS 集群和 ALB 连接在一起:

t.add_resource(ecs.Service(
    "service",
    Cluster=ImportValue(
        Join(
            "-",
            [Select(0, Split("-", Ref("AWS::StackName"))),
                "cluster-id"]
        )
    ),
    DesiredCount=1,
    TaskDefinition=Ref("task"),
    LoadBalancers=[ecs.LoadBalancer(
        ContainerName="helloworld",
        ContainerPort=3000,
        TargetGroupArn=ImportValue(
            Join(
                "-",
                [Select(0, Split("-", Ref("AWS::StackName"))),
                    "alb-target-group"]
            ),
        ),
    )],
    Role=Ref("ServiceRole")
))

最后,像往常一样,我们将使用以下命令输出由我们的代码生成的模板:

print(t.to_json())

脚本现在已准备就绪,并应如下所示: github.com/yogeshraheja/EffectiveDevOpsTemplates/blob/master/helloworld-ecs-service-cf-template.py

现在我们将生成模板并创建堆栈,步骤如下:

$ git add helloworld-ecs-service-cf-template.py
$ git commit -m "Adding helloworld ECS service script"
$ git push
$ python helloworld-ecs-service-cf-template.py > helloworld-ecs-service- cf.template
$ aws cloudformation create-stack \
 --stack-name staging-helloworld-service \
 --capabilities CAPABILITY_IAM \
 --template-body file://helloworld-ecs-service-cf.template \
 --parameters \ ParameterKey=Tag,ParameterValue=latest 

几分钟后,堆栈应该已创建。我们可以回到 ALB 堆栈的输出,获取我们新部署应用程序的 URL 并测试其输出,步骤如下:

$ aws cloudformation describe-stacks \
 --stack-name staging-alb \
 --query 'Stacks[0].Outputs'

[
 {
 "Description": "TargetGroup",
 "ExportName": "staging-alb-target-group",
 "OutputKey": "TargetGroup",
 "OutputValue": "arn:aws:elasticloadbalancing:us-east-
         1:094507990803:targetgroup/stagi-Targe-
         ZBW30U7GT7DX/329afe507c4abd4d"
 },
 {
 "Description": "Helloworld URL",
 "OutputKey": "URL",
 "OutputValue": "http://stagi-LoadB-122Z9ZDMCD68X-1452710042.us-
         east-1.elb.amazonaws.com:3000"
 }
]

$ curl http://stagi-LoadB-122Z9ZDMCD68X-1452710042.us-east-1.elb.amazonaws.com:3000
Hello World
Also the same can be confirmed from the browser. 

这也可以通过浏览器进行确认,如以下截图所示:

我们已经完成了我们的临时 ECS 环境的创建。此时,我们可以轻松地手动将新代码部署到我们的暂存环境,步骤如下:

  1. 在本地修改helloworld代码。例如,将Hello World更改为Hello From Yogesh Raheja,如以下截图所示:

  1. 登录到ecr注册表,步骤如下:
$ eval "$(aws ecr get-login --region us-east-1 --no-include- email)" 
  1. 构建你的 Docker 容器,步骤如下:
$ docker build -t helloworld 
  1. 选择一个新的唯一标签,并使用它来标记你的镜像。例如,假设你的新标签是foobar,如以下代码所示:
$ docker tag helloworld 094507990803.dkr.ecr.us-east-1.amazonaws.com/helloworld:foobar 
  1. 将镜像推送到ecr仓库,步骤如下:
$ docker push 094507990803.dkr.ecr.us-east-1.amazonaws.com/helloworld:foobar
  1. 更新 ECS 服务的 CloudFormation 堆栈,步骤如下:
$ aws cloudformation update-stack \
 --stack-name staging-helloworld-service \
 --capabilities CAPABILITY_IAM \
 --template-body file://helloworld-ecs-service-cf.template \
 --parameters \ 
      ParameterKey=Tag,ParameterValue=foobar 
  1. 更新后检查输出,步骤如下:
$ curl http://stagi-LoadB-122Z9ZDMCD68X-1452710042.us-east-1.elb.amazonaws.com:3000 

Hello From Yogesh Raheja 

浏览器输出也反映了更新后的镜像响应:

使用这一事件序列,我们将自动化部署过程,并创建一个新的持续集成/持续部署(CI/CD)管道。

创建 CI/CD 管道以部署到 ECS

如我们所知,能够在我们的环境中持续部署代码是一个非常强大的工具,因为它有助于打破传统的开发与运维隔阂,并提高新代码发布的速度。我们创建了一个管道,允许我们自动将 Hello World 应用程序的更新部署到我们的自动扩展组用于暂存和生产环境。我们将创建一个类似的管道,但这次它将更新 ECS。我们的 ECS 基础设施将如下所示:

重用上一部分中生成的 CloudFormation 模板,将创建一个与暂存环境相同的生产环境。请注意,ecr 仓库是特定于某个应用程序的,因此我们将在所有环境中共享它。此外,我们将遵循在第三章中学到的最佳实践,将基础设施视为代码,并通过 CloudFormation 堆栈创建我们的管道。我们的第一步将是为生产环境创建一个 ECS 集群。

创建我们的生产 ECS 集群

由于我们事先在 CloudFormation 模板中做的工作,添加新环境将变得非常简单。我们将首先启动一个生产 ECS 集群:

$ aws cloudformation create-stack \
 --stack-name production-cluster \
 --capabilities CAPABILITY_IAM \
 --template-body file://ecs-cluster-cf.template \
 --parameters \     
      ParameterKey=KeyPair,ParameterValue=EffectiveDevOpsAWS \ 
      ParameterKey=VpcId,ParameterValue=vpc-4cddce2a \ 
      ParameterKey=PublicSubnet,ParameterValue=subnet-
      e67190bc\\,subnet-658b6149\\,subnet-d890d3e4\\,subnet-
      6fdd7927\\,subnet-4c99c229\\,subnet-b03baebc
{
 "StackId": "arn:aws:cloudformation:us-east-
     1:094507990803:stack/production-cluster/1e1a87f0-b2da-11e8-8fd2-
     503aca4a58d1"
} 

我们需要等待堆栈创建完成,因为我们需要从集群创建中获取一些导出的值。我们可以运行以下命令,直到能够创建下一个堆栈为止,这样我们的终端就会暂停:

$ aws cloudformation wait stack-create-complete \
 --stack-name production-cluster 

与此同时,我们创建我们的 ALB 并等待创建过程完成:

$ aws cloudformation create-stack \
 --stack-name production-alb \
 --capabilities CAPABILITY_IAM \
 --template-body file://helloworld-ecs-alb-cf.template
{
 "StackId": "arn:aws:cloudformation:us-east-
    1:094507990803:stack/production-alb/bea35530-b2da-11e8-a55e-
    500c28903236"
}

$ aws cloudformation wait stack-create-complete --stack-name production-alb 

最后,我们可以使用以下代码创建我们的服务:

$ aws cloudformation create-stack \
 --stack-name production-helloworld-service \
 --capabilities CAPABILITY_IAM \
 --template-body file://helloworld-ecs-service-cf.template \
 --parameters \ ParameterKey=Tag,ParameterValue=latest
{
 "StackId": "arn:aws:cloudformation:us-east-
     1:094507990803:stack/production-helloworld-service/370a3d40-b2db-
     11e8-80a8-503f23fb5536"
}

$ aws cloudformation wait stack-create-complete \
 --stack-name production-helloworld-service 

此时,我们的生产环境应该已经正常运行。我们可以通过查看 ALB 堆栈创建的输出,获取其 URL,并且可以使用 CURL 访问端点以确保应用程序正在运行:

$ aws cloudformation describe-stacks \
 --stack-name production-alb \
 --query 'Stacks[0].Outputs'
[
 {
 "Description": "TargetGroup",
 "ExportName": "production-alb-target-group",
 "OutputKey": "TargetGroup",
 "OutputValue": "arn:aws:elasticloadbalancing:us-east-
         1:094507990803:targetgroup/produ-Targe-
         LVSNKY9T8S6E/83540dcf2b5a5b54"
 },
 {
 "Description": "Helloworld URL",
 "OutputKey": "URL",
 "OutputValue": "http://produ-LoadB-40X7DRUNEBE3-676991098.us-
         east-1.elb.amazonaws.com:3000"
 }
]

$ curl http://produ-LoadB-40X7DRUNEBE3-676991098.us-east-1.elb.amazonaws.com:3000
Hello World 

输出结果如下:

现在我们的生产环境已准备就绪,我们将着手自动化容器的创建。为了实现这一目标,我们将依赖 CodeBuild 服务。

使用 CodeBuild 自动化创建容器

AWS CodeBuild 是一项托管服务,旨在编译源代码。它类似于 Jenkins,但由于它是符合 AWS 标准的托管服务,因此提供了一组不同的功能和优点。在我们的情况下,使用 CodeBuild 而不是 Jenkins 将使我们能够创建容器,而无需启动和管理额外的 EC2 实例。该服务还与 CodePipeline 很好地集成,正如之前所述,它将驱动我们的流程。

我们将通过 Troposphere 作为中介使用 CloudFormation 来创建我们的 CodeBuild 项目。

我们还将创建一个新的脚本并命名为 helloworld-codebuild-cf-template.py。我们将从通常的导入、模板变量创建和描述开始,如下所示:

"""Generating CloudFormation template."""

from awacs.aws import (
    Allow,
    Policy,
    Principal,
    Statement
)

from awacs.sts import AssumeRole

from troposphere import (
    Join,
    Ref,
    Template
)

from troposphere.codebuild import (
    Artifacts,
    Environment,
    Project,
    Source
)
from troposphere.iam import Role

t = Template()

t.add_description("Effective DevOps in AWS: CodeBuild - Helloworld container")

我们现在将定义一个新角色,以授予我们的 CodeBuild 项目适当的权限。CodeBuild 项目将与多个 AWS 服务进行交互,如 ECR、CodePipeline、S3 和 CloudWatch 日志。为了加快过程,我们将依赖 AWS 默认策略来配置权限。这将给我们以下代码:

t.add_resource(Role(
    "ServiceRole",
    AssumeRolePolicyDocument=Policy(
        Statement=[
            Statement(
                Effect=Allow,
                Action=[AssumeRole],
                Principal=Principal("Service", ["codebuild.amazonaws.com"])
            )
        ]
    ),
    Path="/",
    ManagedPolicyArns=[
        'arn:aws:iam::aws:policy/AWSCodePipelineReadOnlyAccess',
        'arn:aws:iam::aws:policy/AWSCodeBuildDeveloperAccess',
        'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser',
        'arn:aws:iam::aws:policy/AmazonS3FullAccess',
        'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess'
    ]
))

CodeBuild 项目需要定义多个元素。我们首先要定义的是环境。它告诉 CodeBuild 我们需要什么类型的硬件和操作系统来构建我们的项目,以及需要预先安装哪些软件。它还允许我们定义额外的环境变量。我们将使用 AWS 提供的 Docker 镜像,它将为我们提供完成工作所需的一切。该 Docker 镜像预装并配置了 AWS 和 Docker CLI。我们还将定义一个环境变量,以便找到我们的 ecr 仓库端点:

environment = Environment(
    ComputeType='BUILD_GENERAL1_SMALL',
    Image='aws/codebuild/docker:1.12.1',
    Type='LINUX_CONTAINER',
    EnvironmentVariables=[
        {'Name': 'REPOSITORY_NAME', 'Value': 'helloworld'},
        {'Name': 'REPOSITORY_URI',
            'Value': Join("", [
                Ref("AWS::AccountId"),
                ".dkr.ecr.",
                Ref("AWS::Region"),
                ".amazonaws.com",
                "/",
                "helloworld"])},
    ],
)

在 CodeBuild 中,大部分逻辑都定义在一个名为 buildspec 的资源中。buildspec 部分定义了构建的不同阶段以及在这些阶段中要运行的内容。它与我们在第五章 添加持续集成和持续部署 中创建的 Jenkins 文件非常相似。buildspec 部分可以作为 CodeBuild 项目的一部分创建,或作为 YAML 文件添加到正在构建的项目的根目录中。我们将选择第一种方式,并在我们的 CloudFormation 模板中定义 buildspec。我们将创建一个变量并将一个 YAML 字符串存储其中。由于这是一个多行变量,我们将使用 Python 的三引号语法。

我们需要指定的第一个密钥对是模板的版本。当前的 CodeBuild 模板版本是 0.1

buildspec = """version: 0.1

我们的构建过程目标是生成一个新的容器镜像,对其进行标记,并将其推送到 ecr 仓库。这将分三个阶段完成:

  • 预构建:这将生成容器镜像标记并登录到 ECR。

  • 构建:这将构建新的容器镜像。

  • 构建后:这将把新的容器镜像推送到 ECR,并更新 latest 标签以指向新容器。

为了更容易理解每个容器中包含的内容,我们将使用 helloworld 项目中最近 Git 提交的 SHA 对它们进行标记。这有助于理解每个容器的内容,因为我们可以运行类似 git checkout <container tag>git log <container tag> 的命令。由于 CodeBuild 和 CodePipeline 的架构方式,要在 CodeBuild 中获取此标签需要一些工作。我们需要运行以下两个复杂的命令:

  • 第一个阶段将提取当前代码管道执行的执行 ID。这是通过结合使用 AWS CLI 和环境变量 CODEBUILD_BUILD_IDCODEBUILD_INITIATOR 来实现的,这些环境变量在构建开始时由 CodeBuild 服务定义。

  • 接下来,我们将使用该执行 ID 提取工件修订 ID,恰好是我们要寻找的提交 SHA。

这些命令使用了 --query 过滤选项的一些高级功能。你可以通过以下链接了解更多信息:amzn.to/2k7SoLE

在 CodeBuild 中,每个命令都在其自己的环境中运行,因此跨步骤共享数据的最简单方法是使用临时文件。

buildspec版本定义之后,添加以下内容以生成预构建阶段的第一部分并提取标签:

phases:
  pre_build:
    commands:
      - aws codepipeline get-pipeline-state --name "${CODEBUILD_INITIATOR##*/}" --query stageStates[?actionStates[0].latestExecution.externalExecutionId==\`$CODEBUILD_BUILD_ID\`].latestExecution.pipelineExecutionId --output=text > /tmp/execution_id.txt
      - aws codepipeline get-pipeline-execution --pipeline-name "${CODEBUILD_INITIATOR##*/}" --pipeline-execution-id $(cat /tmp/execution_id.txt) --query 'pipelineExecution.artifactRevisions[0].revisionId' --output=text > /tmp/tag.txt

我们的标签现在已出现在/tmp/tag.txt文件中。接下来,我们需要生成两个文件,如下所示:

  • 第一个文件将包含docker tag命令的参数(这将类似于<AWS::AccountId>.dkr.ecr.us-east-1.amazonaws.com/helloworld:<tag>)。为此,我们将利用之前在模板中定义的环境变量。

  • 第二个文件将是一个 JSON 文件,它将定义一个包含标签的键值对。稍后我们将在将新容器部署到 ECS 时使用该文件。

在执行了前面的命令后,添加以下命令以生成这些文件:

printf "%s:%s" "$REPOSITORY_URI" "$(cat /tmp/tag.txt)" > /tmp/build_tag.txt
      - printf '{"tag":"%s"}' "$(cat /tmp/tag.txt)" > /tmp/build.json

为了结束pre_build部分,我们将登录到我们的ecr仓库:

- $(aws ecr get-login --no-include-email)

我们现在将定义我们的构建阶段。感谢之前创建的build_tag文件,构建阶段将变得非常简单。我们将以与本章第一部分类似的方式调用docker build命令:

 build:
    commands:
      - docker build -t "$(cat /tmp/build_tag.txt)" .

我们将添加post_build阶段以完成构建。在这一部分,我们将把新构建的容器推送到我们的ecr仓库,具体如下:

post_build:
    commands:
      - docker push "$(cat /tmp/build_tag.txt)"
      - aws ecr batch-get-image --repository-name $REPOSITORY_NAME --image-ids imageTag="$(cat /tmp/tag.txt)" --query 'images[].imageManifest' --output text | tee /tmp/latest_manifest.json
      - aws ecr put-image --repository-name $REPOSITORY_NAME --image-tag latest --image-manifest "$(cat /tmp/latest_manifest.json)"

除了各个阶段外,buildspec中还定义了一个artifacts部分。该部分用于定义在构建成功后需要上传到 S3 的内容,以及如何准备它。我们将导出build.json文件,并将discard-paths变量设置为 true,以便不保留/tmp/目录的信息。最后,我们将关闭我们的三重引号字符串,如下所示:

artifacts:
  files: /tmp/build.json
  discard-paths: yes
"""

现在我们的buildspec变量已定义,我们可以添加 CodeBuild 项目资源。通过实例化该项目,我们将为项目设置一个名称,调用之前定义的变量设置其环境,设置服务角色,并配置源和工件资源,这些资源定义了如何处理构建过程及其输出:

t.add_resource(Project(
    "CodeBuild",
    Name='HelloWorldContainer',
    Environment=environment,
    ServiceRole=Ref("ServiceRole"),
    Source=Source(
        Type="CODEPIPELINE",
        BuildSpec=buildspec
    ),
    Artifacts=Artifacts(
        Type="CODEPIPELINE",
        Name="output"
    ),
))

和往常一样,我们将以以下print命令结束脚本的创建:

print(t.to_json()) 

我们的脚本现在已完成,应该如下所示:github.com/yogeshraheja/EffectiveDevOpsTemplates/blob/master/helloworld-codebuild-cf-template.py

我们可以保存文件,将其添加到 git 中,生成 CloudFormation 模板,并如下创建我们的堆栈:

$ git add helloworld-codebuild-cf-template.py
$ git commit -m "Adding CodeBuild Template for our helloworld application"
$ git push
$ python helloworld-codebuild-cf-template.py > helloworld-codebuild- cf.template
$ aws cloudformation create-stack \
 --stack-name helloworld-codebuild \
 --capabilities CAPABILITY_IAM \
 --template-body file://helloworld-codebuild-cf.template 

几分钟之内,我们的堆栈就会创建完成。接下来,我们希望利用它。为此,我们将再次使用 CodePipeline,创建一个全新的、容器感知的管道。

使用 CodePipeline 创建我们的部署管道

我们将使用 AWS CodePipeline 构建一个与第五章《添加持续集成和持续部署》中创建的管道非常相似的管道:

我们将从一个 Source 步骤开始,在此步骤中我们将连接到 GitHub,并在代码更改时自动触发新的管道。之后,我们将构建一个新的容器,并将其推送到我们的 ecr 仓库,依赖于我们刚刚创建的 CodeBuild 项目。然后,我们将把新的容器部署到暂存环境。为了做到这一点,我们将使用 CodePipeline 提供的 CloudFormation 集成功能,结合在我们的 CodeBuild 项目的 buildspec 部分生成的 build.json 文件。你可能还记得,我们的 helloworld 服务模板将标签作为部署参数。我们将触发一个堆栈更新操作,并用 build.json 文件中定义的值覆盖该参数的默认值。之后,我们将在触发相同的部署之前添加一个手动审批步骤,但这次是部署到生产环境。

通过 CodePipeline 部署和更新 CloudFormation 模板将需要在输入中指定模板的位置。为了方便提供它,我们将首先通过将 CloudFormation 模板添加到我们的源中来开始。

将 CloudFormation 模板添加到我们的代码库中

ECS 的变更由我们在 helloworld-ecs-service-cf.template 文件中定义的任务定义驱动。到目前为止,我们只在 GitHub 上存储了我们的 Python 脚本。我们需要为该模板制作一个特殊情况,并存储其 JSON 输出,以便 CodePipeline 能与我们的堆栈交互。我们将把这个文件添加到我们的 Git 仓库中,放在一个新目录下,如下所示:

$ cd helloworld
$ mkdir templates
$ curl -L https://raw.githubusercontent.com/yogeshraheja/EffectiveDevOpsTemplates/master/helloworld-ecs-service-cf-template.py | python > templates/helloworld-ecs-service-cf.template
$ git add templates
$ git commit -m "Adding CloudFormation template for the helloworld task"
$ git push 

现在我们的模板已经存在于源中,我们可以为我们的管道创建 CloudFormation 模板。

为 CodePipeline 创建 CloudFormation 模板

我们将从在 EffectiveDevOpsTemplates 本地创建一个名为 helloworld-codepipeline-cf-template.py 的文件开始。

我们将从我们的样板代码开始编写脚本:

"""Generating CloudFormation template."""

from awacs.aws import (
    Allow,
    Policy,
    Principal,
    Statement,
)
from awacs.sts import AssumeRole
from troposphere import (
    Ref,
    GetAtt,
    Template,
)
from troposphere.codepipeline import (
    Actions,
    ActionTypeId,
    ArtifactStore,
    InputArtifacts,
    OutputArtifacts,
    Pipeline,
    Stages
)
from troposphere.iam import Role
from troposphere.iam import Policy as IAMPolicy

from troposphere.s3 import Bucket, VersioningConfiguration

t = Template()

t.add_description("Effective DevOps in AWS: Helloworld Pipeline")

我们将创建的第一个资源是 S3 存储桶,管道将使用它来存储每个阶段生成的所有工件。我们还将开启该存储桶的版本控制:

t.add_resource(Bucket(
    "S3Bucket",
    VersioningConfiguration=VersioningConfiguration(
        Status="Enabled",
    )
))

我们现在将创建所需的 IAM 角色,如下所示:

  1. 我们将定义的第一个角色将是用于 CodePipeline 服务的角色:
t.add_resource(Role(
    "PipelineRole",
    AssumeRolePolicyDocument=Policy(
        Statement=[
            Statement(
                Effect=Allow,
                Action=[AssumeRole],
                Principal=Principal("Service", ["codepipeline.amazonaws.com"])
            )
        ]
    ),
    Path="/",
    Policies=[
        IAMPolicy(
            PolicyName="HelloworldCodePipeline",
            PolicyDocument={
                "Statement": [
                    {"Effect": "Allow", "Action": "cloudformation:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "codebuild:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "codepipeline:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "ecr:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "ecs:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "iam:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "s3:*", "Resource": "*"},
                ],
            }
        ),
    ]
))
  1. 第二个角色将由部署阶段用于执行 CloudFormation 变更:
t.add_resource(Role(
    "CloudFormationHelloworldRole",
    RoleName="CloudFormationHelloworldRole",
    Path="/",
    AssumeRolePolicyDocument=Policy(
        Statement=[
            Statement(
                Effect=Allow,
                Action=[AssumeRole],
                Principal=Principal(
                    "Service", ["cloudformation.amazonaws.com"])
            ),
        ]
    ),
    Policies=[
        IAMPolicy(
            PolicyName="HelloworldCloudFormation",
            PolicyDocument={
                "Statement": [
                    {"Effect": "Allow", "Action": "cloudformation:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "ecr:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "ecs:*", "Resource": "*"},
                    {"Effect": "Allow", "Action": "iam:*", "Resource": "*"},
                ],
            }
        ),
    ]
))
  1. 现在我们可以创建我们的管道资源。我们将首先配置其名称,并指定我们刚刚创建的角色的 Amazon 资源名称ARN):
t.add_resource(Pipeline(
    "HelloWorldPipeline",
    RoleArn=GetAtt("PipelineRole", "Arn"),
  1. 接下来,我们将引用之前创建的 S3 存储桶,以便为通过管道执行产生的不同工件提供存储位置:
 ArtifactStore=ArtifactStore(
        Type="S3",
        Location=Ref("S3Bucket")
  1. 现在我们将定义管道的每个阶段。CloudFormation 结构反映了我们之前使用 Web 界面所做的操作。每个阶段都有一个唯一的名称,并由多个动作组成。每个动作由一个名称、一个类别、一个配置,以及可选的输入和输出工件定义:

我们的第一个阶段将是 GitHub 阶段,如下所示:

Stages=[
        Stages(
            Name="Source",
            Actions=[
                Actions(
                    Name="Source",
                    ActionTypeId=ActionTypeId(
                        Category="Source",
                        Owner="ThirdParty",
                        Version="1",
                        Provider="GitHub"
                    ),
                    Configuration={
                        "Owner": "ToBeConfiguredLater",
                        "Repo": "ToBeConfiguredLater",
                        "Branch": "ToBeConfiguredLater",
                        "OAuthToken": "ToBeConfiguredLater"
                    },
                    OutputArtifacts=[
                        OutputArtifacts(
                            Name="App"
                        )
                    ],
                )
            ]
        ),
  1. 我们将创建一个名为App的第一个工件,包含存储库的内容。为了避免硬编码任何OAuthToken,我们将在创建 CloudFormation 堆栈后配置 GitHub 集成。

我们的下一步将是配置构建。如前所述,我们将简单地调用上一节中启动的 CodeBuild 堆栈。我们将把输出工件存储为BuildOutput,这意味着现在我们有两个工件:App工件和BuildOutput,后者包含由 CodeBuild 生成的tag.json文件:

Stages(
            Name="Build",
            Actions=[
                Actions(
                    Name="Container",
                    ActionTypeId=ActionTypeId(
                        Category="Build",
                        Owner="AWS",
                        Version="1",
                        Provider="CodeBuild"
                    ),
                    Configuration={
                        "ProjectName": "HelloWorldContainer",
                    },
                    InputArtifacts=[
                        InputArtifacts(
                            Name="App"
                        )
                    ],
                    OutputArtifacts=[
                        OutputArtifacts(
                            Name="BuildOutput"
                        )
                    ],
                )
            ]
        ),
  1. 我们现在将创建暂存部署。与之前不同,我们不再使用 CodeDeploy,而是直接更新 CloudFormation 模板。为了实现这一点,我们需要将模板的位置提供给我们的操作配置。由于我们将其添加到helloworld GitHub 仓库中,因此可以借助App工件来引用它。我们的模板位于<directory root>/templates/helloworld-ecs-service-cf.template,对于 CodePipeline 来说就是App::templates/helloworld-ecs-service-cf.template

配置 CloudFormation 操作的下一个技巧依赖于我们可以覆盖为堆栈提供的参数这一事实。CloudFormation 提供了一些函数来帮助处理动态参数。你可以在amzn.to/2kTgIUJ了解更多关于这些函数的内容。我们在这里将重点介绍一个函数:Fn::GetParam。此函数从工件中存在的键值对文件中返回一个值。这就是我们利用在 CodeBuild 中创建的文件的地方,因为该文件将包含一个 JSON 字符串,格式为 { "tag": "<latest git commit sha>" }

Stages(
            Name="Staging",
            Actions=[
                Actions(
                    Name="Deploy",
                    ActionTypeId=ActionTypeId(
                        Category="Deploy",
                        Owner="AWS",
                        Version="1",
                        Provider="CloudFormation"
                    ),
                    Configuration={
                        "ChangeSetName": "Deploy",
                        "ActionMode": "CREATE_UPDATE",
                        "StackName": "staging-helloworld-ecs-service",
                        "Capabilities": "CAPABILITY_NAMED_IAM",
                        "TemplatePath": "App::templates/helloworld-ecs-service-cf.template",
                        "RoleArn": GetAtt("CloudFormationHelloworldRole", "Arn"),
                        "ParameterOverrides": """{"Tag" : { "Fn::GetParam" : [ "BuildOutput", "build.json", "tag" ] } }"""
                    },
                    InputArtifacts=[
                        InputArtifacts(
                            Name="App",
                        ),
                        InputArtifacts(
                            Name="BuildOutput"
                        )
                    ],
                )
            ]
        ),
  1. 在暂存部署完成后,我们将请求手动批准,如下所示:
 Stages(
            Name="Approval",
            Actions=[
                Actions(
                    Name="Approval",
                    ActionTypeId=ActionTypeId(
                        Category="Approval",
                        Owner="AWS",
                        Version="1",
                        Provider="Manual"
                    ),
                    Configuration={},
                    InputArtifacts=[],
                )
            ]
        ),
  1. 最后,我们将创建最后一个阶段来运行生产部署。此处的代码与暂存阶段完全相同,只是阶段的名称和我们的配置所针对的堆栈不同:
Stages(
            Name="Production",
            Actions=[
                Actions(
                    Name="Deploy",
                    ActionTypeId=ActionTypeId(
                        Category="Deploy",
                        Owner="AWS",
                        Version="1",
                        Provider="CloudFormation"
                    ),
                    Configuration={
                        "ChangeSetName": "Deploy",
                        "ActionMode": "CREATE_UPDATE",
                        "StackName": "production-helloworld-ecs-service",
                        "Capabilities": "CAPABILITY_NAMED_IAM",
                        "TemplatePath": "App::templates/helloworld-ecs-service-cf.template",
                        "RoleArn": GetAtt("CloudFormationHelloworldRole", "Arn"),
                        "ParameterOverrides": """{"Tag" : { "Fn::GetParam" : [ "BuildOutput", "build.json", "tag" ] } }"""
                    },
                    InputArtifacts=[
                        InputArtifacts(
                            Name="App",
                        ),
                        InputArtifacts(
                            Name="BuildOutput"
                        )
                    ],
                )
            ]
        )
    ],
))
  1. 我们的管道资源现在已经创建完毕。我们可以通过打印出我们的模板来结束脚本的创建:
print(t.to_json()) 

脚本现在已准备好使用。它应与以下链接中的脚本相似:github.com/yogeshraheja/EffectiveDevOpsTemplates/blob/master/helloworld-codepipeline-cf-template.py

我们现在可以创建我们的管道。

启动并配置我们的 CloudFormation 堆栈

我们将按常规步骤继续创建管道的第一部分,如下所示:

$ git add helloworld-codepipeline-cf-template.py
$ git commit -m "Adding Pipeline to deploy our helloworld application using ECS"
$ git push
$ python helloworld-codepipeline-cf-template.py > helloworld-codepipeline- cf.template
$ aws cloudformation create-stack \
 --stack-name helloworld-codepipeline \
 --capabilities CAPABILITY_NAMED_IAM \
 --template-body file://helloworld-codepipeline-cf.template 

在这种情况下,我们使用CAPABILITY_NAMED_IAM功能,因为我们在 IAM 级别定义了自定义名称。

这将创建我们的管道。然而,有一个小问题是,我们在管道中没有指定 GitHub 凭证。这是因为我们不想在 GitHub 中以明文存储它。AWS 在 IAM 中提供了一项加密服务,但本书不会涉及这部分内容。因此,我们将在第一次编辑管道时按如下方式进行操作:

  1. 在浏览器中打开 console.aws.amazon.com/codepipeline

  2. 选择你新创建的管道

  3. 点击顶部的编辑

  4. 点击 GitHub 操作中的铅笔图标:

  1. 点击右侧菜单中的连接 GitHub,并按照步骤授权 AWS CodePipeline

  2. 在仓库步骤中选择你的helloworld项目和主分支

  3. 点击更新,保存管道更改,最后,点击保存并继续

几秒钟后,你的管道将触发,你应该能看到第一次部署正在进行。这标志着我们 CI/CD 管道的创建完成:

你还可以在 AWS 控制台上看到所有 CloudFormation 堆栈的详细信息,状态为 CREATE_COMPLETE,如下所示:

总结

在本章中,我们探讨了容器的概念,使用 Docker 和 ECS。在了解 Docker 工作原理的基础上,我们为我们的应用程序创建了一个容器。将其在本地运行后,我们创建了一组新的资源,在 AWS 上运行 Docker 容器。我们通过 DevOps 最佳实践来完成此操作,并使用 CloudFormation 生成资源,将基础设施视为代码。这使我们能够将这些更改保存在源代码控制中。在资源方面,我们创建了一个 ECR 仓库来管理不同版本的容器。我们还创建了两个 ECS 集群,具备自动扩展功能,用于暂存和生产环境,两个 ALB 用于将流量代理到容器,一个任务集和一个 ECS 服务,用于配置和部署我们的应用程序。

最后,我们重新实现了一个 CI/CD 管道。我们通过使用 CodeBuild、CodePipeline 及其与 CloudFormation 的集成来完成这一工作。

我们将继续改进我们的系统,并实现 DevOps 最后一项关键特性之一:衡量一切。通过利用我们使用的不同服务中存在的一些功能,并结合其他 AWS 服务(例如 CloudWatch),我们将能够为我们的基础设施和服务实施监控策略。

问题

  1. 什么是 Docker?列出 Docker 引擎的重要组件。

  2. 你能在你选择的任何支持的平台/操作系统上安装并配置最新版本的 Docker CE 吗?

  3. 你能创建一个 Docker 镜像并使用相同的镜像创建一个 Web 服务器容器吗?

  4. 你能通过 AWS Web 控制台创建 ECR 和 ECS,以熟悉 ECS 术语吗?

进一步阅读

请参考以下链接获取更多信息:

第八章:加强 AWS 环境的安全性

在本章中,我们将重点讨论如何确保我们的 AWS 账户和应用程序的安全。云计算和安全性是两个并不总是兼容的概念。这并非由于云的本质,而是因为人们认为本地服务器比云上的服务器更安全。因为你确切知道本地服务器的位置以及如何与它建立连接。本章的目的是通过一些实际的工具和信息,证明一个良好管理的 AWS 云环境可以比本地环境更安全。

首先,我们将研究如何为 IAM 用户确保访问安全。接着,我们将探讨如何启用 CloudTrail 记录 IAM 的使用日志,并在网络层面通过 VPC 流日志进行监控。在将应用程序和基础设施部署到云之前,创建正确的子网是至关重要的一步。最后,我们将探索 AWS 提供的一个强大工具——Web 应用防火墙WAF)。

最重要的安全原则之一是最小权限原则。这意味着将用户的访问权限限制到完成其工作所需的最小权限。

在本章中,我们将在 AWS 基础设施的多个层级实施这一过程。接下来,我们将更详细地探讨以下主题:

  • 身份与访问管理IAM)安全性

  • CloudTrail

  • 虚拟私有云VPC)子网

  • AWS WAF

技术要求

本章中包含的代码文件可以在 GitHub 上找到,链接为:github.com/giuseppeborgese/effective_devops_with_aws__second_edition

IAM 安全性

IAM 使你能够安全地控制对 AWS 服务的访问。在这里,我们需要实施最小权限原则,并通过记录所有用户的操作来监控谁在做什么。

根账户

当你创建一个 AWS 账户并使用根账户登录时,你将看到类似于以下截图的内容:

执行 IAM web 控制台所建议的所有操作非常重要,同时也要更改根账户的密码。

根账户密码

首先,更改根账户的密码。在页面的右上角,铃铛图标和全球下拉菜单之间,你将找到你的 AWS 别名或账户号码。点击它,然后点击“我的账户”选项:

接下来,点击“编辑”按钮。其他步骤比较直接和合乎逻辑,如下所示:

出于安全原因,网页会要求您再次提供登录信息。仅有密码保护是不够的,尤其是对于根账户;您应该绝对启用多因素认证MFA),无论您是使用虚拟设备还是硬件设备。市场上有许多解决方案。例如,Google Authenticator 是安卓设备中最著名的应用之一。我也使用过 Yubico(www.yubico.com/)生产的物理安全密钥。

删除根账户的访问密钥

访问密钥的权限与使用密码访问时授予的权限相同,因此,当根账户的此类访问被移除,只留下密码访问时,能够创建一个更安全的环境(除非在某些特殊情况下)。不要担心以下截图中显示的消息:

如果您为根账户创建了访问密钥,并发现该密钥已被删除,系统将显示以下消息:

为 IAM 用户设置密码策略

您应该应用的密码策略取决于您希望对 IAM 用户密码应用的安全级别。我建议以下类似的配置,但它会根据您的具体用例有所不同:

创建管理员组和个人 IAM 用户

为了更安全地操作根账户,最好创建一个个人 IAM 用户并通过该用户进行操作。将权限分配给组而不是直接分配给 IAM 用户也是最佳实践。操作方法如下:

  1. 创建一个名为 admins 或类似名称的组。

  2. 将管理员策略分配给该组。

  3. 根据某些标准创建个人 IAM 用户。以我为例,我会选择 myname.mysurname giuseppe.borgese

  4. 将新的 IAM 用户添加到 admins 组。

这允许其他 IAM 用户评估是否创建权限比管理员少的组,也允许他们分配必要的权限,但不会多于需要的权限。例如,如果一个 IAM 用户需要管理 EC2 实例,我们可以给他们预定义的 AmazonEC2FullAccess 策略;如果他们需要管理 RDS 环境,则可以给他们 AmazonRDSFullAccess 策略。

AmazonEC2FullAccess 策略

需要根账户访问权限的任务在 AWS 文档页面中列得非常清楚,网址是 docs.aws.amazon.com/general/latest/gr/aws_tasks-that-require-root.html。以下是这些任务的列表:

  • 修改根用户详情

  • 更改 AWS 支持计划

  • 关闭 AWS 账户

  • 注册 GovCloud

  • 提交 Amazon EC2 请求的反向 DNS

  • 创建 CloudFront 密钥对

  • 创建 AWS 创建的 X.509 签名证书

  • 将 Route 53 域名转移到另一个 AWS 账户

  • 更改 Amazon EC2 设置以支持更长的资源 ID

  • 请求移除 EC2 实例上25端口的电子邮件限制

  • 查找你的 AWS 账户规范用户 ID

所有这些操作都非常罕见,因此在日常任务中遇到这些事件是不寻常的。

最终安全状态

现在,所有任务已经完成,你可以从 root 用户注销,开始使用你创建的具有管理员权限的 IAM 用户,如下所示:

如果你对云计算方法完全陌生,建议花一些时间阅读AWS 共享责任模型,地址为aws.amazon.com/compliance/shared-responsibility-model/。在页面上,有清晰的定义,说明了 AWS 的责任是(云的安全性),同时也定义了我们的责任是(云中的安全性)。简而言之,我们的责任是确保我们在云中创建的内容,以及用于创建这些内容的所有工具,都符合 AWS 的安全要求。

过去,著名的 AWS S3 服务曾发生过许多安全漏洞,因为人们将该服务配置为可以从世界任何地方进行读写。AWS 保证该服务始终保持更新和修补,但我们在访问时所赋予的权限仍由我们自己掌控。

在 YouTube 上,你可以听到 Kate Turchin 的一首非常好听的歌,地址为www.youtube.com/watch?v=tIb5PGW_t1o。这首歌以通俗易懂的方式解释了共享责任模型:

CloudTrail

我们已启用 IAM 个人用户,并避免使用 root 账户。我们还为我们的组分配了必要的 IAM 策略,并将每个用户分配到正确的组。然而,我们也需要记录他们的所有操作。为实现这个目的,启用的 AWS 服务是 CloudTrail。

每个由 IAM 用户或分配了 IAM 角色的资源在 AWS 基础设施上执行的操作都会记录在 S3 桶中和/或 CloudWatch 日志组中。我的建议是参考 AWS 文档:docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-create-a-trail-using-the-console-first-time.html。如果你阅读了这篇文档,通过网页控制台创建跟踪将非常简单。

VPC 流日志

入侵检测系统IDS)和 入侵预防系统IPS)是安全网络中常见的工具。在本地环境中,它们不那么容易或便宜实现,因为您需要专用硬件,以及适合此功能的网络结构。相比之下,在 AWS 中,只需使用 VPC 服务的一个特性,您可以在任何时候和任何地点启用和禁用这些工具。您可以在网络的三个级别上拥有这些工具:

  • VPC 级别

  • 子网级别

  • 弹性网络接口ENI)级别

如您所知,网络接口属于一个子网,而一个子网属于一个 VPC。因此,如果在子网级别启用工具,则无需在网络接口级别应用它们,如果在 VPC 级别启用它们,则无需在子网级别应用它们。在激活此功能之前,您需要创建以下三个资源:

  • 一个空的 CloudWatch 组,数据将存储在那里

  • 用于执行 VPC 流日志操作的 AWS 角色

  • 与角色相关联的策略,带有必要的权限

当然,您可以手动创建这些资源,并且所有执行此操作的说明都可以在流日志文档页面上找到:docs.aws.amazon.com/AmazonVPC/latest/UserGuide/flow-logs.html。但是,为了采用更 DevOps/自动化的方法,我们可以使用 Terraform 模块。在这种情况下,我们使用在 GitHub 上创建的远程模块。正如您可以在有关模块来源的官方 Terraform 文档中看到的那样:www.terraform.io/docs/modules/sources.html#github,GitHub 是一个支持的来源类型。但是,如果您想使用自己的 GitHub 仓库,可以使用 sshhttps 作为模块源。有关更多信息,请参阅:www.terraform.io/docs/modules/sources.html#github

调用模块的代码非常简单,仅需要两个参数——sourceprefixprefix 将用于命名所有模块资源。您可以下载或查看在 技术要求 部分提供的 GitHub 仓库链接,详细了解此模块的功能。但是,要使用它,以下几行就足够了:

module "flow-log-prerequisite" {
   source = "github.com/giuseppeborgese/effective_devops_with_aws__second_edition//terraform- modules//vpc-flow-logs-prerequisite"
   prefix = "devops2nd"
 }
output "role" { value = "${module.flow-log-prerequisite.rolename}" }
output "loggroup" { value = "${module.flow-log-prerequisite.cloudwatch_log_group_arn}" }

然后,在 Web 控制台中使用输出中的名称非常有用。

当您将模块行添加到任何现有文件中或具有 .tf 扩展名的新文件中后,需要使用 terraform init 进行初始化。

以下是 terraform init 命令的输出:

terraform init -upgrade
 Upgrading modules...
 - module.flow-log-prerequisite
 Updating source "github.com/giuseppeborgese/effective_devops_with_aws__second_edition//terraform-modules//vpc-flow-logs-prerequisite"
Initializing the backend...
Initializing provider plugins...
....

terraform 二进制文件刚刚下载了模块代码。此时,如果之前没有进行此操作,它将从最新的可用版本下载 AWS 提供程序信息。-upgrade 选项强制您使用最新的可用版本,因此通常这是一个好主意。

现在,通过 terraform plan,我们可以看到将创建的三个对象:

terraform plan -out /tmp/tf11.out
 Refreshing Terraform state in-memory prior to plan...
 The refreshed state will be used to calculate this plan, but will not 
 be persisted to local or remote state storage.
...
...
An execution plan has been generated and is shown below.
 Resource actions are indicated with the following symbols:
 + create
Terraform will perform the following actions:
+ module.flow-log-prerequisite.aws_cloudwatch_log_group.flow_log
 id: <computed>
 arn: <computed>
 name: "devops2nd_flowlogs"
 retention_in_days: "0"
+ module.flow-log-prerequisite.aws_iam_role.flow_role
 id: <computed>
 arn: <computed>
 assume_role_policy: "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"\",\n \"Effect\": \"Allow\",\n \"Principal\": {\n \"Service\": \"vpc-flow-logs.amazonaws.com\"\n },\n \"Action\": \"sts:AssumeRole\"\n }\n ]\n}\n"
 create_date: <computed>
 force_detach_policies: "false"
 max_session_duration: "3600"
 name: "devops2nd_flowlogs"
 path: "/"
 unique_id: <computed>
+ module.flow-log-prerequisite.aws_iam_role_policy.flow_policy
 id: <computed>
 name: "devops2nd_flowlogs"
 policy: "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Action\": [\n \"logs:CreateLogGroup\",\n \"logs:CreateLogStream\",\n \"logs:PutLogEvents\",\n \"logs:DescribeLogGroups\",\n \"logs:DescribeLogStreams\"\n ],\n \"Effect\": \"Allow\",\n \"Resource\": \"*\"\n }\n ]\n}\n"
 role: "${aws_iam_role.flow_role.id}" Plan: 3 to add, 0 to change, 0 to destroy.
-----------------------------------------------------------------------

该计划已保存至:/tmp/tf11.out

要应用这些操作,请运行以下命令:

terraform apply /tmp/tf11.out

然后,使用 terraform apply 命令创建它们:

tf11 apply /tmp/tf11.out
 module.flow-log-prerequisite.aws_cloudwatch_log_group.flow_log: Creating...
 arn: "" => "<computed>"
 name: "" => "devops2nd_flowlogs"
 retention_in_days: "" => "0"
 module.flow-log-prerequisite.aws_iam_role.flow_role: Creating...
 arn: "" => "<computed>"
 assume_role_policy: "" => "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"\",\n \"Effect\": \"Allow\",\n \"Principal\": {\n \"Service\": \"vpc-flow-logs.amazonaws.com\"\n },\n \"Action\": \"sts:AssumeRole\"\n }\n ]\n}\n"
 create_date: "" => "<computed>"
 force_detach_policies: "" => "false"
 max_session_duration: "" => "3600"
 name: "" => "devops2nd_flowlogs"
 path: "" => "/"
 unique_id: "" => "<computed>"
 module.flow-log-prerequisite.aws_iam_role.flow_role: Creation complete after 2s (ID: devops2nd_flowlogs)
 module.flow-log-prerequisite.aws_iam_role_policy.flow_policy: Creating...
 name: "" => "devops2nd_flowlogs"
 policy: "" => "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Action\": [\n \"logs:CreateLogGroup\",\n \"logs:CreateLogStream\",\n \"logs:PutLogEvents\",\n \"logs:DescribeLogGroups\",\n \"logs:DescribeLogStreams\"\n ],\n \"Effect\": \"Allow\",\n \"Resource\": \"*\"\n }\n ]\n}\n"
 role: "" => "devops2nd_flowlogs"
 module.flow-log-prerequisite.aws_cloudwatch_log_group.flow_log: Creation complete after 3s (ID: devops2nd_flowlogs)
 module.flow-log-prerequisite.aws_iam_role_policy.flow_policy: Creation complete after 1s (ID: devops2nd_flowlogs:devops2nd_flowlogs)
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
 Outputs:
loggroup = arn:aws:logs:us-east-1:790419456202:log-group:devops2nd_flowlogs:*
 role = devops2nd_flowlogs

请记下最后两条输出内容,因为我们需要激活流日志。

为一个子网创建流日志

现在,在所有先决条件都满足的情况下,我们将为在 AWS 网页控制台中打开的 VPC 服务中的一个子网创建流日志:

  1. 选择一个子网。然后,选择 "Flow Logs" 标签,点击 "Create flow log" 按钮,如下图所示:

  1. 按照以下截图中提供的信息进行填写。日志组和角色是使用 Terraform 模块创建的。在这个示例中,我们关心的是查看被接受的流量,因此我们在筛选器下拉菜单中选择了 "Accept" 选项:

现在,您在 AWS 网页控制台中看到这样的情况时,请注意子网编号,因为在验证时我们将需要它。当然,您的子网 ID 会与我的不同,我的 ID 是 subnet-15a59419

验证流日志

为了验证流日志是否正常工作,并获得流日志的实践经验,我们将为该子网创建一个 EC2 实例,并通过 SSH 登录,分析该 SSH 登录的流量。

我们不会在这里涵盖创建 EC2 实例的完整过程,因为这是一个基础任务。如果您已经阅读到本书的这一部分,您应该已经知道如何操作了。我建议使用一个 t2.micro 类型,它符合免费套餐的资格。此外,非常重要的一点是,您必须在刚刚激活流日志的子网上创建机器,并允许从您的位置访问 SSH。

几分钟后,您可以进入 CloudWatch 服务,点击 "Logs" 选项,选择通过 Terraform 创建的日志组 devops2nd_flowlogs

在其中,您将找到与之前创建的 EC2 实例关联的网络接口名称,如下图所示:

如果您在同一子网中有多个网络接口,这意味着您有多个机器,您需要进入 EC2 服务并选择网络接口选项,利用实例 ID 列来定位网络接口,如下图所示:

然而,你可能只有一个网络接口,所以点击它的名称。在我的例子中,这是 eni-0d899a52e790058aa-accept

有许多行;要了解每一行的详细信息,你可以查看记录文档,访问 docs.aws.amazon.com/AmazonVPC/latest/UserGuide/flow-logs.html#flow-log-records

然而,我们想要查找我们的 SSH 连接尝试,因此有必要通过像 www.whatsmyip.org/ 这样的服务找出我们笔记本的公网 IP,并将其放入过滤器中,如下所示:

在第一行中,你可以看到以下项目:

  • 我的笔记本的公网 IP 是 79.1.172.1

  • EC2 实例的私有 IP 是 172.31.61.129

  • 我的笔记本的源端口是 61704

  • EC2 实例的 SSH 服务的目标端口是端口 22

VPC 流日志的考虑事项

我们已经完成了 VPC 流日志服务的概念验证(PoC)。当然,服务中还有许多其他选项,你可以在官方的 AWS 文档中找到它们。通过访问这些文档,你可以继续探索 VPC 流日志的潜力。

在此时,如果你尝试在本地环境中完成与 VPC 流日志相同的任务,应该能清楚地意识到,与在本地环境中启用完全流量监控相比,在 AWS 云上启用它是多么简单。

别忘了删除之前创建的 EC2 实例,以避免产生不必要的额外费用。其他资源不会产生费用,除非你在该子网中生成了大量流量。

VPC 子网

在本节中,我们将看看如何按照最小权限原则组织我们的 VPC 子网。我们必须在最少的情况下暴露并授予访问权限给我们的资源(EC2、ELB 和 RDS),以限制安全攻击和数据泄露。

在每个 AWS 区域中,已经创建了一个默认 VPC。如果你想了解更多关于它的详细信息,我建议你阅读 默认 VPC 和默认子网 文档,地址是 docs.aws.amazon.com/AmazonVPC/latest/UserGuide/default-vpc.html。不过,简而言之,可以说,任何放在这里的东西如果你配置的安全组允许,都是有可能暴露给公网的。

路由和子网类型

在官方文档docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Scenarios.html中,描述了你 VPC 配置的四种场景,查阅这些内容会很有帮助。理解你在子网中放置的资源的访问方式由以下三个因素决定是很重要的:

  • 路由

  • 网络访问控制NAC)列表(无状态防火墙)

  • 安全组(有状态防火墙)

我的建议是不要修改 NAC;保持默认的 NAC 附加到每个子网,它允许所有的入站和出站流量,并使用安全组作为防火墙。根据安全级别,子网可以分为三种类型:

  • 公有子网

  • 具有互联网访问的私有子网

  • 没有互联网访问的私有子网

访问私有子网

公有子网中的资源可以通过使用公共 IP 并启用安全组接收连接来访问。对于私有子网,你至少有三种方式可以做到这一点,如下所示:

  • 跳转到一个公有子网中的堡垒主机,然后从那里访问私有资源。

  • 使用 AWS VPN 服务的站点到站点 VPN,docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpn-connections.html,连接到你办公室中的物理路由器/设备。你可以连接两个路由器以增加冗余。

  • 在 EC2 实例中放置虚拟 VPN 软件,并将设备连接到它。有无数的解决方案可以做到这一点,许多都在 AWS 市场上,按月收费,可以直接使用。

如果你有带物理路由器的办公室,首选方案始终是站点到站点的解决方案。

在哪个子网中放置什么资源?

在我的测试 VPC 中,我有六个子网——每种类型有两个子网,您可以在以下截图中看到:

你应该在每种类型的子网中放置什么? 请考虑以下几点:

  • 公有子网:指的是所有具有公共访问权限的外部弹性负载均衡ELB),堡垒主机(如果有的话),位于 EC2 实例中的虚拟 VPN 软件,以及任何需要从互联网访问的资源,且无法通过其他方式访问。

  • 具有互联网访问的私有子网:指的是所有内部 ELB 以及所有位于 ELB(内部或外部)后面的 EC2 实例,这些实例需要从互联网下载或上传内容,以及需要从互联网下载或上传内容的数据库。

  • 没有互联网访问的私有子网:指的是所有不需要任何理由访问互联网的资源,以及那些从内部仓库下载更新的资源

在 Web 控制台中识别子网

请记住以下几点:

  • 每个子网可以关联一个路由表

  • 一个路由表可以与多个子网关联

  • 如果您没有明确将路由表关联到子网,默认的路由表会自动关联

在以下截图中,您可以看到三个路由表,其中公共路由是默认的路由表:

在子网部分,您可以看到与该子网关联的路由表及其单个路由,但要更改路由表的内容,您必须从路由表部分进行编辑。私有路由与公共路由表/子网的区别在于0.0.0.0/0路由的目标。如果是一个转发互联网网关,igw-xxxxx意味着这个子网可以从外部世界访问,并且能够连接到互联网(假设安全组允许这样做):

如果它指向 NAT 网关或其他 EC2 实例,这意味着它是一个具有互联网访问权限的私有子网,并且可以通过任何方式访问互联网,因此它可以从外部世界访问。首先,您必须点击“创建 NAT 网关”按钮,如下所示:

之后,您可以更改路由表,并出现如以下截图所示的情况:

如果0.0.0.0/0不在,如上截图所示,它是一个完全私有的子网:

端点路由

如果数据库必须将备份上传到同一区域的私有 S3 存储桶,则绝不应使用互联网访问,而应采用私有内部路由。这就是所谓的 VPC 端点。通过这种路由,您可以避免通过互联网访问 AWS 服务,如 S3、DynamoDB 或 CloudWatch,从而提高速度、安全性并节省成本(互联网流量是有费用的)。要查看所有支持 VPC 端点的服务,您可以查看官方文档:docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-endpoints.html

在这里,我们将配置第一个可用的 S3 服务 VPC 端点,如下所示:

  1. 转到 VPC | 端点 | 创建端点:

  1. 保留默认的 AWS 服务并选择 S3 服务,如下截图所示:

  1. 选择您正在使用的 VPC,并修改所有路由表:

  1. 现在,您可以看到一个新的路由规则,如下截图所示:

请记住,这将适用于在同一 VPC 区域中创建的所有存储桶。在这个例子中,区域是us-east-1,即北弗吉尼亚。

在 AWS 文档的 Endpoints for Amazon S3 部分 docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-endpoints-s3.html,有一个关于 S3 存储桶策略的有趣的强化规则,如下所示:

{
    "Version": "2012-10-17",
    "Id": "Policy1415115909152",
     "Statement": [
        {
            "Sid": "Access-to-specific-VPCE-only",
            "Principal": "*",
            "Action": "s3:*",
            "Effect": "Deny",
            "Resource": ["arn:aws:s3:::my_secure_bucket",
                         "arn:aws:s3:::my_secure_bucket/*"],
            "Condition": {
                "StringNotEquals": {
     "aws:sourceVpce": "vpce-039f31bfec07367ea"
                 }
             }
        }
       ]
 }

我通过添加我的 VPC 端点 ID vpce-039f31bfec07367ea 替换了文档中的那个。根据此规则,my_secure_bucket 存储桶只能从与该端点关联的 VPC 访问。

AWS WAF

您可以通过使用安全组和私有子网限制访问所有资源。所有监视日志、VPC 流日志和 CloudTrail 都是活跃的。IAM 策略已强制执行,所有内容都得到了正确的安全保护,没有任何内容暴露。这是因为您有 VPN 服务可以访问任何资源。但是,如果您想提供 Internet 服务,您必须至少打开一个对外界的访问点。正如我们在 VPC 子网 部分已经讨论过的那样,应该尽可能少地在公共子网中暴露资源,只有一个 ELB 应该处于此状态,将连接传递给私有子网中的 EC2 机器,因为 EC2 机器使用严格的安全规则与 RDS 数据库进行通信。

这是最经典的 AWS 应用程序之一,在此无需详细解释。相反,我们希望关注如何通过 AWS WAF 增强 ELB 的安全性。有关更多信息,请参阅 aws.amazon.com/waf/。AWS WAF 是一种在应用程序级别工作的防火墙,可以在 TCP/IP 协议栈的第 7 层进行保护,而不是在第 4 层(安全组所在层)。

WAF 能做什么安全组不能做到? 考虑以下问题的答案:

  • 防止 SQL 注入和跨站脚本攻击

  • 阻止拒绝服务DoS)和分布式拒绝服务DDoS

  • 保护您 Web 应用程序 URL 的一部分,例如 www.mywebsite/admin

在本章中,我们将使用 Terraform 创建两个关于 DoS 和子 URL 的实用 POC。为此,我们将创建一个 Web 应用程序沙盒,应用 WAF,并测试触发其保护规则。请注意,在撰写本书的这一部分时,WAF 仅适用于应用负载均衡器ALB)和 CloudFront,但 AWS 不断更新其服务,所以未来可能会有所改变。

Web 应用程序沙盒

我们的测试沙盒将是一个 ALB 和一个安装了 Apache2 Web 服务器的 EC2 机器。在本节中,我们仅创建环境并进行测试,不涉及任何 WAF 配置。但在下一节中,我们将在 ALB 上添加 WAF 级别。

要创建以下沙盒,我们将使用 GitHub 上提供的一个 Terraform 模块:

首先,在你的 main.tf 文件中添加以下代码:

module "webapp-playground" {
  source = "github.com/giuseppeborgese/effective_devops_with_aws__second_edition//terraform-modules//webapp-playground"
  subnet_public_A = "subnet-a94cabf4"
  subnet_public_B = "subnet-54840730"
  subnet_private = "subnet-54840730"
  vpc_id = "vpc-3901d841"
  my_ami = "ami-b70554c8"
  pem_key_name = "effectivedevops"
}

需要记住的一些事项如下:

  • ALB 必须始终位于至少两个子网中,且分别位于两个不同的可用区。

  • 这个 ALB 可以通过端口 80 访问,并使用 HTTP 监听器,这对于 PoC(概念验证)来说是可以接受的。然而,在你的真实环境中,最好通过 AWS Route 53 注册一个公共域名,使用 AWS Certificate Manager 创建一个 SSL 证书,将这个证书关联到 ALB,并使用 HTTPS 监听器。

  • 安全组设置非常严格,你可以查看模块代码,看到 ALB 的安全组入口仅从端口 80 到整个互联网 0.0.0.0/0 可达,并且只能从 EC2 安全组的端口 80 进行 出口 访问。

像往常一样,要创建资源,你需要运行以下命令:

terraform init -upgrade
terraform plan -out /tmp/tf11.out
Plan: 12 to add, 0 to change, 0 to destroy. 
terraform apply /tmp/tf11.out

找到 DNS ALB 名称,如下截图所示,并复制它:

通过浏览器打开它,如下所示:

还有一个子目录,我们可以稍后用来测试我们的 WAF:

如果你没有足够快地使用它,你的 playground 会通过 terraform 命令销毁它,以避免产生任何不必要的费用。如果你只想销毁 playground,而不销毁其他已创建的资源,你可以使用选择性 destroy 模块,如下所示:

terraform destroy -target module.webapp-playground

如果你确认选择 Yes,那么将销毁这 12 个模块资源。

允许只有来自某个 IP 的子 URL 可访问

通常,在你的 Web 应用程序中,你有一个管理区域,可能这个部分的门户并不是所有人都能访问的。当然,你有用户名和密码,但攻击者可以通过多种方式窃取这些凭证。

如果这是一个敏感应用程序,为了遵循最小权限原则,限制访问仅限于需要使用这种管理访问的位置是很方便的;例如,从办公室。如果你可以为管理区域设置不同的访问权限,你可以将其放在内部负载均衡器后面,并通过使用 VPN 服务将 VPC 连接到你的办公室,正如之前的部分所讨论的那样。内部负载均衡器的 DNS 名称会转换为你的 VPC 的私有 IP 地址,通过这种方式,你可以确保连接仅来自受信任的来源,例如你的办公室。

然而,很多时候你没有这个选项,因为应用程序是一个整体打包的,无法将管理子 URL 与主部分分开。在这种情况下,唯一可用的修改是使用 AWS WAF,并仅对管理子 URL 应用过滤器。我们需要创建一个 WAF 并将其附加到 ALB 上,以供我们的 Web 应用程序使用。

为此,我创建了一个 terraform 模块,你可以通过以下几行代码将其用于你的代码中:

module "limit_admin_WAF" {
  source = "github.com/giuseppeborgese/effective_devops_with_aws__second_edition//terraform-modules//limit-the-admin-area"
  alb_arn = "${module.webapp-playground.alb_arn}"
  my_office_ip = "146.241.179.87/32"
  admin_suburl = "/subdir"
}

当然,不要忘记将你的办公 IP 或家庭连接的公共 IP 替换为my_office_ip字段中的内容,如果是单一 IP,就像我的情况一样,使用子网掩码/32

命令和往常一样,如下所示:

terraform plan -out /tmp/tf11.out
terraform init -upgrade
terraform apply /tmp/tf11.out 

为了方便测试,我添加了一个alb_url输出变量,如下所示:

alb_url = playground-1940933132.us-east-1.elb.amazonaws.com
loggroup = arn:aws:logs:us-east-1:790419456202:log-group:devops2nd_flowlogs:*
role = devops2nd_flowlogs
giuseppe@Giuseppes-MacBook-Air ~/p/effectivedevops>

现在,WAF 已与 ALB 关联,所有请求都将被过滤。

使用命令行进行测试

这次,我们将使用命令行工具curl进行测试。从我的办公 IP 可以看到,当尝试访问根目录和子 URL 时,未出现任何问题:

giuseppe@Giuseppes-MacBook-Air ~> curl http://playground-1940933132.us-east-1.elb.amazonaws.com/subdir/
 This is a sub directory
giuseppe@Giuseppes-MacBook-Air ~> curl http://playground-1940933132.us-east-1.elb.amazonaws.com/
 This is a playground main directory
giuseppe@Giuseppes-MacBook-Air ~>

相反,当我使用带有另一个公共 IP 的虚拟机时,得到了以下结果:

[ec2-user@ip-172-31-6-204 ~]$ curl http://playground-1940933132.us-east-1.elb.amazonaws.com/
 This is a playground main directory
 [ec2-user@ip-172-31-6-204 ~]$ curl http://playground-1940933132.us-east-1.elb.amazonaws.com/subdir/
 <html>
 <head><title>403 Forbidden</title></head>
 <body bgcolor="white">
 <center><h1>403 Forbidden</h1></center>
 </body>
 </html>

从 Web 控制台识别 WAF

查看所创建的资源,也就是服务 WAF,如下所示:

查看 Web ACL 选项,并从过滤器菜单中选择你所在的区域。你可以看到 Terraform 模块所创建的内容,如下所示:

在“规则”部分,可以看到过滤器本身,以及允许访问受限区域的 IP:

阻止 DoS/DDoS 攻击

DoS 攻击是我们应用程序中的老问题,尤其是在分布式版本中,即 DDoS 攻击,其中多个来源(通常是许多被黑客攻击的设备,组成一个僵尸网络)试图通过同时发出大量查询来发起 DoS 攻击,导致网络过载。在这种情况下,为了防御并继续为合法用户提供流量服务,识别并阻止恶意源是至关重要的。

值得花一点时间阅读关于AWS 上拒绝服务攻击缓解的官方文档,aws.amazon.com/answers/networking/aws-ddos-attack-mitigation/。我们在这里要做的是专注于使用 WAF 的实际示例。

AWS WAF 可以阻止发送过多请求的单一公共 IP。你可能会问,多少请求算太多? 这个取决于你的 Web 应用程序,因此在应用任何此类过滤器之前,你需要测量来自单一 IP 在五分钟时间范围内接收到的请求数量。

请记住,AWS WAF 的下限是 2000 个请求,并且根据我的测试,虽然请求2001不会被阻止,但一段时间后,你会看到许多后续请求被阻止。就像我们对其他示例所做的那样,我们不会完全相信 AWS 的声明,而是会在创建后测试我们的 PoC。为了立即看到系统是否在工作,我们将为我们的子 URLhttp://playground-1940933132.us-east-1.elb.amazonaws.com/subdir/设置 AWS 限制。我们不会对主页http://playground-1940933132.us-east-1.elb.amazonaws.com应用任何限制。

使用 Terraform 创建 AWS WAF

销毁 limit_admin_WAF 模块以避免冲突。你可以使用以下命令进行操作:

terraform destroy -target module.limit_admin_WAF

接下来,在你的代码中使用 /* */ 注释掉模块:

/* module "limit_admin_WAF" {
 source
.............
} */

使用以下代码创建新的模块:

 module "limit_admin_WAF" {
 source = "github.com/giuseppeborgese/effective_devops_with_aws__second_edition//terraform-modules//ddos_protection"
 alb_arn = "${module.webapp-playground.alb_arn}"
 admin_suburl = "subdir"
 }

如往常一样,包含以下代码片段:

terraform init --upgrade
terraform plan -out /tmp/tf11.out
terraform apply /tmp/tf11.out
 Outputs:
alb_url = playground-1757904639.us-east-1.elb.amazonaws.com

从输出中获取 DNS 名称,并使用 curl 命令测试一切是否正常,具体如下:

 curl playground-1757904639.us-east-1.elb.amazonaws.com

以下是 playground 主目录:

curl playground-1757904639.us-east-1.elb.amazonaws.com/subdir/ 

这是一个子目录。登录到 Web 控制台,进入 WAF 服务,选择弗吉尼亚区域,并注意 Rate-based 类型的 subdir 规则,如下图所示:

此外,在“规则”部分,你会注意到目前没有任何 IP 被阻止:

请记住,AWS 默认禁止任何形式的 DoS 测试,并且可能会被阻止,因为它违反了条款和条件。有关 AWS 服务条款 的更多信息,请参阅 aws.amazon.com/service-terms/。在我们的案例中,我们将从单个 IP 在短时间内发送 2,000/4,000 个请求。这么大数量的请求不会触发 AWS 的警报。如果你有非常好的互联网连接,可以从你的笔记本电脑运行此脚本,但我的建议是使用直接暴露在互联网上的 Amazon Linux EC2 机器,这样我们能在相同的实验条件下进行操作。

登录到你的机器,并使用以下命令下载脚本:

curl -O https://raw.githubusercontent.com/giuseppeborgese/effective_devops_with_aws__second_edition/master/terraform-modules/ddos_protection/test_protection.sh
chmod +x test_protection.sh
./test_protection.sh ...

这将向你的 ALB 测试环境发送 4,000 个请求。从输出中,你可以看到前 2,000/3,000 个请求将会成功:

This is a subdirectory
 538
This is a subdirectory
 539
This is a subdirectory
 540
This is a subdirectory
 541
This is a subdirectory

然而,你将开始收到如下的拒绝请求:

259
 <html>
 <head><title>403 Forbidden</title></head>
 <body bgcolor="white">
 <center><h1>403 Forbidden</h1></center>
 </body>
 </html>
 260
 <html>
 <head><title>403 Forbidden</title></head>
 <body bgcolor="white">
 <center><h1>403 Forbidden</h1></center>
 </body>
 </html>

如果你在第一次运行时没有看到此项,你需要重新运行脚本来触发请求。你可以通过 WAF 服务中的 Web 控制台登录,然后在“规则”部分看到你的 EC 机器的公共 IP:

然而,如果你运行 curl 请求根目录,你会看到它仍然可以从你的 EC2 机器访问。如果你尝试从你的笔记本电脑访问,subdir URL 仍然可以访问。如果你暂时不再发送请求,你 EC2 机器的公共 IP 会从黑名单中移除,这是正确的,因为如果该 IP 恢复正常流量传输,它就不再是威胁。

DDoS 攻击考虑

AWS WAF 是缓解 DoS 和 DDoS 攻击的非常有用的工具,但在开始使用之前,建议执行以下操作:

  • 阅读并观察如何在 AWS 上实现 DoS 攻击缓解

  • 了解你的应用程序,并为并发连接设置合理的限制,避免阻塞有效流量并获得误报响应

  • 构建一个可扩展的 web 应用程序,以响应请求,直到 WAF 理解到它正受到攻击并触发其过滤器。

SQL 注入(SQLi)的 WAF 防护

我们创建并测试了 WAF 的速率规则和子 URL 限制功能。正如我们在开始时所说的,还有 SQLi 功能,并且可以在官方 AWS 网站的github.com/aws-samples/aws-waf-sample GitHub 仓库中找到与此相关的一些 CloudFormation 模板。

总结

在本章中,我们在不同级别应用了最小权限原则。在 IAM 部分,您学习了如何锁定根账户并将控制权交给 IAM 用户,通过配置密码策略、设置权限和创建组。通过启用 CloudTrail,我们跟踪并监控了由 IAM 用户或服务在我们环境中执行的每个操作。使用 VPC Flow Logs,我们观察到适用于 VPC 任意位置的强大网络监控工具,并且我们还使用 Terraform 创建了我们的前提条件,Terraform 是一个用于扩展我们实践的绝佳工具。我们还介绍了 Terraform 模块的概念。在 VPC 子网 部分,我们研究了可以在 AWS 云中使用的三种子网类型,以及如何将不同种类的资源放置在我们的基础设施中,尽量减少将其暴露到互联网,并尽可能将其保留在私有区域。

在讨论 WAF 服务时,我们探索了 AWS 世界中最强大的安全服务之一。保护 Web 应用程序的某些敏感部分是非常有用的。DoS 保护是专业 Web 服务中应该始终存在的东西。配置 WAF 并不总是容易的,但得益于 Terraform 自动化的强大功能以及本书中提供的 PoC 模块,理解原理并进行相应配置只需一些 terraformgit 命令即可。

问题

  1. 假设我刚刚注册了 AWS 云并通过电子邮件收到了我的密码。我可以开始构建我的基础设施吗?还是必须先遵循一些最佳实践?

  2. 我应该在我的 AWS 账户中启用哪种类型的登录?

  3. 安全组和 NACL 是 AWS 中唯一的防火墙吗?

  4. 我如何使用 AWS 保护我的 web 应用程序免受 DDoS 攻击?

  5. 我可以将所有资源放在一个子网中吗?

深入阅读

安全是一个非常广泛的领域,一章内容无法面面俱到。更多资源可以在aws.amazon.com/whitepapers/aws-security-best-practices/找到。

互联网安全中心CIS)的 AWS 基础安全基准是一个用于保护 AWS 账户/环境的安全加固指南。请参考以下链接:

有关更多关于 AWS 认证安全专家 的信息,请参考 aws.amazon.com/certification/certified-security-specialty/

第九章:评估

第一章: 云计算与 DevOps 革命

  1. DevOps 是一个框架和方法论,旨在为开发人员和运维团队之间的协作采纳合适的文化。

  2. DevOps – IaC 代表 DevOps – 基础设施即代码,我们应该将我们的垂直基础设施视为代码来管理和处理,这有助于实现可重复、可扩展和可管理的基础设施。

  3. DevOps 文化的关键特征

    • 版本控制所有内容

    • 自动化测试

    • 自动化配置

    • 配置管理

    • 自动化部署

    • 测量

    • 适应虚拟化(公有/私有云)

  4. 云中的三种主要服务模型:

    • 基础设施即服务 (IaaS)

    • 平台即服务 (PaaS)

    • 软件即服务 (SaaS)

  5. AWS 是当前最大的公有云服务平台。AWS 提供多种服务,从计算、存储到机器学习和分析,所有这些服务都具有高度的可扩展性和可靠性。使用 AWS 最重要的部分是 按需付费模式。你无需投资硬件,直接部署服务并按使用时长付费。当你关闭并移除服务时,不会产生费用——这点非常棒。

第二章: 部署你的第一个 Web 应用程序

  1. 如果你没有 AWS 云账户,访问 www.aws.amazon.com 并创建一个免费套餐账户。按照aws.amazon.com/上的逐步指南操作。你需要提供信用卡或借记卡信息才能创建 AWS 账户。

  2. 访问 console.aws.amazon.com 并选择 AWS 计算服务来创建你的第一个 EC2 实例。在控制台上点击 启动实例 按钮,并按照步骤选择 AMI、实例类型(在此选择免费套餐),接着选择实例详情、存储详情、标签和安全组。在此练习中,你可以选择默认选项,因为我们的目标仅仅是熟悉控制台门户,以便通过 DevOps 实践实现自动化。

  3. 按照本章中 创建我们的第一个 Web 服务器 部分提供的逐步指南,使用 AWS CLI 创建你的第一个 AWS 实例。

  4. 跟随本章中 创建一个简单的 Hello World Web 应用程序 部分的步骤。你可以从以下链接下载应用程序的示例代码:

  5. 使用ec2-metadata --instance-id查找你的 AWS 实例 ID,然后通过修改你的实例 ID 执行以下命令:aws ec2 terminate-instances --instance-ids <YOUR AWS INSTANCE ID>

第三章:将基础设施视为代码

  1. IaC 代表基础设施即代码(Infrastructure as Code)。这是一个将你的基础设施对象(例如 EC2 实例、VPC 网络、子网、负载均衡器、存储、应用部署和编排)作为基础设施代码进行处理的过程。IaC 允许在非常短的时间内对整个环境中的基础设施垂直进行更改、复制和回滚。

  2. 打开 CloudFormation 模板,访问console.aws.amazon.com/cloudformation,然后点击“创建堆栈”按钮。接下来,使用位于raw.githubusercontent.com/yogeshraheja/Effective-DevOps-with-AWS/master/Chapter03/EffectiveDevOpsTemplates/helloworld-cf-template-part-1.py的 Python 文件,创建一个helloworld-cf.template模板文件。完成后,将模板上传至 Amazon S3。为你的堆栈提供一个名称,然后添加一个 SSH 密钥对及其他可以默认的附加信息。现在审查信息并点击“创建”。当模板创建完成后,点击“输出”标签,点击 Weburl 链接,它将带你到应用程序主页。

提示:通过将脚本的输出保存到python helloworld-cf-template.py > helloworld-cf.template文件中生成 CloudFormation 模板。

  1. 市场上有多个 SCM(源代码管理)产品,包括 GitLab、BitBucket、GitHub,甚至公共云提供的 SCM 服务。在这里,我们将使用最流行的 SCM 服务之一:GitHub。请在github.com上创建一个免费的 GitHub 账户。完成后,登录到你的 GitHub 账户,创建第一个名为helloworld的公共仓库。

  2. 为您的支持平台安装 Git 包,并使用 git clone <github repository URL> 克隆之前创建的 GitHub 仓库,您可以在 GitHub 控制台中找到该仓库的 URL。现在,将您的 helloworld-cf.template 文件复制到仓库中,并进行 git addgit commit 操作。现在,您可以将本地仓库文件推送到您的 GitHub 账户。为此,执行 git push 推送已提交的文件,并通过检查 GitHub 仓库确认。

  3. Ansible 是一款简单、强大且易于学习的配置管理工具,广泛应用于系统/云工程师和 DevOps 工程师,用于自动化处理他们日常的重复性任务。Ansible 的安装非常简单,采用无代理模式运行。

    在 Ansible 中,模块是创建 Ansible 代码文件(YAML 格式)的基本构件。这些文件被称为 Ansible Playbooks(Ansible 剧本)。多个 Ansible 剧本按定义良好的目录结构组织,在 Ansible 中称为 roles,其中 roles 是存放 Ansible 代码的目录结构,包括 Ansible 剧本、变量、静态/动态文件等。Ansible 中还包含许多其他对象,包括 Ansible Vault、Ansible Galaxy,以及一个名为 Ansible Tower 的 Ansible 图形界面。您可以通过访问 docs.ansible.com 进一步探索这些对象。

第四章:使用 Terraform 实现基础设施即代码

  1. Terraform 是一款高级基础设施工具,主要用于安全、高效地构建、修改和版本控制基础设施。Terraform 不是配置管理工具,因为它专注于基础设施层,并允许诸如 Puppet、Chef、Ansible 和 Salt 等工具执行应用程序部署和编排。

  2. HashiCorp 并未为操作系统提供原生包。Terraform 以单个二进制文件的形式分发,打包在一个 ZIP 压缩包中,可以从www.terraform.io/downloads.html下载。下载后,解压 .zip 文件并将其放置到 /usr/bin Linux 二进制路径下。完成此操作后,运行 terraform -v 来确认安装的 Terraform 版本。

  3. 为了使用 Terraform 配置 AWS 实例,您需要通过在 .tf 文件中创建 provider 块来初始化 AWS 提供程序。然后运行 terraform init。初始化成功后,您需要继续开发一个 Terraform 模板,并使用 resources。在这种情况下,您需要使用 aws_instance 资源类型及其适当的属性。完成此操作后,进行验证并规划,最后应用您的 Terraform 模板来创建您的第一个 AWS 实例。

  4. 为了使用 Ansible 配置 Terraform,你需要使用一个 provider 来初始化平台;使用 resources 来创建与平台相关的服务;最后使用 provisioner 来与创建的服务建立连接,安装 Ansible,并运行 ansible-pull 在系统上执行 Ansible 代码。你可以参考以下链接获取一个示例 Terraform 模板:raw.githubusercontent.com/yogeshraheja/EffectiveDevOpsTerraform/master/fourthproject/helloworldansiblepull.tf

第五章:添加持续集成和持续部署

  1. CI、CD 和持续交付这些术语可以定义如下:

    • 持续集成:CI 流水线将使我们能够自动和持续地测试提议的代码更改。这将节省开发人员和 QA 的时间,他们不再需要进行那么多手动测试。它还使得代码更改的集成变得更加容易。

    • 持续部署:在 CD 中,你大大加速了 DevOps 提供的反馈回路过程。以高速将新代码发布到生产环境可以让你收集真实的客户指标,这往往会暴露出新的和意想不到的问题。

    • 持续交付:为了构建我们的持续交付流水线,我们首先将为生产环境创建一个 CloudFormation 堆栈。然后,我们将在 CodeDeploy 中添加一个新的部署组,这将使我们能够将代码部署到新的 CloudFormation 堆栈中。最后,我们将升级流水线,加入审批流程,以便将代码部署到生产环境,并包括生产部署阶段本身。

  2. Jenkins 是最广泛使用的集成工具之一,用于运行我们的 CI 流水线。经过超过 10 年的开发,Jenkins 长期以来一直是实践持续集成的领先开源解决方案。Jenkins 以其丰富的插件生态系统而闻名,并经历了一个重大的新版本发布(Jenkins 2.x),该版本将焦点放在了一些非常 DevOps 相关的功能上,包括能够创建本地交付流水线,并进行检查和版本控制。它还提供了与源代码控制系统(如 GitHub)更好的集成。

  3. 为了实现我们的持续部署流水线,我们将关注两个新的 AWS 服务——CodePipeline 和 CodeDeploy:

    • CodePipeline 让我们创建我们的部署流水线。我们将告诉它像之前一样从 GitHub 获取我们的代码,并将其发送到 Jenkins 进行 CI 测试。然而,Jenkins 不再仅仅返回结果到 GitHub,我们将会在 AWS CodeDeploy 的帮助下将代码部署到我们的 EC2 实例。

    • CodeDeploy是一项服务,让我们可以将代码正确地部署到 EC2 实例上。通过添加一定数量的配置文件和脚本,我们可以使用 CodeDeploy 可靠地部署和测试代码。得益于 CodeDeploy,我们无需担心部署顺序中的任何复杂逻辑。它与 EC2 紧密集成,知道如何在多个实例之间执行滚动更新,并且如果需要,还能进行回滚操作。

更多详细信息,请参考本章的构建持续部署流水线部分。

第六章:扩展你的基础设施

  1. 不,这并不总是最佳选择,因为多层应用意味着需要管理更多的组件。如果你的应用在单体架构下运行良好,且你能接受短暂的停机时间,并且流量不会随着时间增加,那么你可以考虑让它继续按现状运行。

  2. 在本书使用的多层架构方法中,所有软件都在一个 ZIP 文件中,而在微服务和无服务器架构中,软件被拆分为多个部分。例如,在一个电子商务软件(用于向用户展示内容的服务)中,管理后端以添加新产品的部分是一个服务,而管理支付的部分是另一个服务,等等。

  3. 如果你不熟悉该服务,可能会觉得有些困难。然而,AWS 提供了大量文档和视频资源。此外,在本书中,我们展示了如何使用一组基本服务在多层架构中打破经典的单体架构方法。

  4. 对于 NLB,这个说法是正确的,但如果你使用 ALB 或 CLB,必须先预热。如果流量每五分钟增长超过 50%,你也需要这样做。

  5. 使用证书管理器是免费的,除非你需要请求一个私有证书,经典的 SSL 证书每年也可能需要 500 美元。

  6. 每个 AWS 区域都由可用区(AZ)组织,每个可用区都是一个独立的数据中心。因此,某一个可用区发生问题的情况很少发生,但同一时刻在多个可用区发生问题的概率较低。每个子网只能属于一个可用区,因此将每个组件至少放置在两个,或者最好放置在三个可用区中,会更加方便。

第七章:在 AWS 中运行容器

  1. Docker 是一个容器平台,用于构建、分发和运行容器化应用程序。Docker 引擎的四个重要组件如下:
    • 容器:读写模板

    • 图像:只读模板

    • 网络:容器的虚拟网络

    • :容器的持久存储

  1. Docker CE 可以在多种平台上安装,包括 Linux、Windows 和 MacOS。请参考docs.docker.com/install/,这是官方的 Docker 链接,点击选择你的平台,然后按照说明安装并配置 Docker CE 的最新版本。

通过运行docker --version命令,确认已安装的 Docker CE 版本。

  1. 使用 Dockerfile github.com/yogeshraheja/helloworld/blob/master/Dockerfile 并使用docker build命令创建镜像。这个新创建的镜像是用于 Hello World 应用程序的镜像。通过使用docker run -d -p 3000:3000 <image-name>暴露外部端口来创建容器。完成后,使用curl或通过浏览器访问公共 IP 和端口3000来检查并确认 Web 服务器的输出。

  2. 使用您的凭证登录到 AWS 账户,并从服务选项卡中选择 ECS 服务。在这里,您会看到创建 Amazon ECS 集群和 Amazon ECR 仓库的选项。此时,点击“仓库”并创建您的第一个 ECR 仓库。屏幕上还会显示一些您可以用于对 ECR 执行操作的命令。类似地,点击“集群”选项卡,然后在 ECS 屏幕上点击“创建集群”。在这里,选择 Windows、Linux 或仅网络的集群选项,点击“下一步”,并填写您的选择的详细信息。这些信息包括集群名称、配置模型、EC2 实例类型、实例数量等。完成该过程后,点击“创建”。几分钟后,您的 ECS 集群将准备就绪。在本章中,我们通过 CloudFormation 展示了这一过程。如果您有兴趣使用相同的流程设置 ECS 集群,可以按照本章创建 ECS 集群部分提供的步骤操作。

第八章:加强您的 AWS 环境的安全性

  1. 在开始构建基础设施之前,强烈建议您锁定根账户(即与注册邮箱绑定的账户)。然后,创建具有必要权限的 IAM 用户和组,并为根账户和 IAM 用户启用多因素认证(MFA),而不仅仅是用户名和密码。

  2. 您应该启用 CloudTrail 以注册 IAM 用户和角色的操作,并启用 VPC 流日志以监控和记录网络流量。

  3. 不是的;另外还有 WAF,一个在 TCP/IP 协议第 7 层工作的应用程序防火墙。

  4. 您必须遵循一些最佳实践来配置应用程序,将应用程序暴露给互联网的表面降到最低,并实现水平扩展。同时,也有 WAF 速率规则帮助限制恶意的 DDoS 攻击。

  5. 理论上,您可以这么做,但将它们分布在私有和公共子网之间更为方便,以仅将必要的资源暴露到互联网。其他任何资源都应该保持私密。此外,最佳做法是将您的应用程序的各部分分布在多个可用区中。这意味着实际上是使用多个数据中心。基于这些原因,并且由于一个子网只能位于一个可用区中,您必须使用多个子网。

posted @ 2025-06-29 10:38  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报