AWS-DevOps-实现指南-全-
AWS DevOps 实现指南(全)
原文:
annas-archive.org/md5/6e8ad498503ec3a019af15fac0c16681
译者:飞龙
序言
DevOps和AWS是近年来在科技行业中逐渐增长的两个关键主题,且有充分的理由。
DevOps 正在逐渐成为事实上的方法论或框架,并被各类规模的组织采纳。它使技术团队能够更高效地工作,并通过加速开发人员与最终用户之间的反馈循环,使工作更加有意义。团队成员通过增强的协作,享有更愉快、更高效的工作环境。
本书将首先探讨DevOps背后的哲学,然后继续通过一些实际示例介绍其最流行的原则。
AWS如今几乎成为了云计算的代名词,凭借其 31%的市场份额位居行业榜首。从 2006 年开始,Amazon Web Services已经发展成一个庞大、独立、复杂的云生态系统。它以惊人的速度推出新服务。AWS的产品类别从原始计算和数据库资源到存储、分析、AI、游戏开发、移动服务和物联网(IoT)解决方案等应有尽有。
我们将使用AWS作为平台应用DevOps技术。在接下来的章节中,我们将看到AWS的便利性和弹性如何极大地补充了DevOps在系统管理和应用开发中的创新方法。
本书内容
第一章,什么是 DevOps,您需要关心吗?,介绍了DevOps哲学。
第二章,开始将您的基础设施视为代码,提供了如何使用Terraform或CloudFormation将基础设施作为代码进行部署的示例。
第三章,将您的基础设施纳入配置管理,展示了如何使用SaltStack配置EC2实例。
第四章,通过持续集成加速构建、测试和发布,描述了如何使用Jenkins CI服务器设置CI工作流的过程。
第五章,随时准备通过持续交付进行部署,展示了如何扩展CI管道,以使用Packer和Serverspec生成可部署的EC2 AMI。
第六章,持续部署 - 完全自动化工作流,提供了一个完全自动化的工作流,并通过添加AMI部署所需的功能来完成CI/CD管道。
第七章,度量、日志收集和监控,介绍了Prometheus、Logstash、Elasticsearch及相关的DevOps工具。
第八章, 优化规模与成本,提供了如何在规划AWS部署时考虑可扩展性和成本效率的建议。
第九章, 保护你的 AWS 环境,介绍了提高AWS部署安全性的最佳实践。
第十章, AWS 技巧与窍门,包含了一些适用于从初学者到中级AWS用户的实用技巧。
本书所需的内容
本书中的实际示例涉及到使用AWS资源,因此需要一个AWS账户。
示例中使用的客户端工具,如AWS CLI和Terraform,在大多数常见操作系统(Linux/Windows/Mac OS)上均受支持。
本书适用对象
本书面向那些管理 AWS 基础设施和环境的系统管理员和开发人员,并计划在组织中实施 DevOps 的人。那些准备获得 AWS 认证 DevOps 工程师认证的人也会觉得本书非常有用。期望具备操作和管理 AWS 环境的先前经验。
约定
本书中,你将看到一些文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义解释。
文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“我们需要通过 SSH 连接到节点,并检索存储在/var/lib/jenkins/secrets/initialAdminPassword
中的管理员密码。”
代码块的设置如下:
aws-region = "us-east-1"
vpc-cidr = "10.0.0.0/16"
vpc-name = "Terraform"
aws-availability-zones = "us-east-1b,us-east-1c"
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会以粗体显示:
aws-region = "us-east-1"
vpc-cidr = "10.0.0.0/16"
vpc-name = "Terraform"
aws-availability-zones = "us-east-1b,us-east-1c"
任何命令行输入或输出都按如下方式写出:
$ terraform validate
$ terraform plan
Refreshing Terraform state prior to plan...
...
Plan: 11 to add, 0 to change, 0 to destroy.
$ terraform apply
aws_iam_role.jenkins: Creating...
...
Apply complete! Resources: 11 added, 0 changed, 0 destroyed.
Outputs:
JENKINS EIP = x.x.x.x
VPC ID = vpc-xxxxxx
新术语和重要词汇以粗体显示。你在屏幕上看到的词语,比如菜单或对话框中的内容,会像这样显示在文本中:“我们选择Pipeline作为作业类型,并为其命名。”
注意
警告或重要的注释会以框的形式出现,如下所示。
提示
提示和技巧如下所示。
读者反馈
我们非常欢迎读者的反馈。请告诉我们你对本书的看法——你喜欢什么或不喜欢什么。读者的反馈对我们非常重要,它帮助我们开发出能真正让你受益的书籍。
要向我们发送一般反馈,只需通过电子邮件发送至 feedback@packtpub.com,并在邮件主题中提到书名。
如果你在某个领域拥有专业知识,并且有兴趣写书或为书籍贡献内容,请查看我们的作者指南:www.packtpub.com/authors。
客户支持
既然你已经是一本 Packt 书籍的骄傲拥有者,我们有很多方式帮助你从购买中获得最大价值。
下载示例代码
您可以从您的帐户中下载本书的示例代码文件,网址是www.packtpub.com
。如果您在其他地方购买了本书,可以访问www.packtpub.com/support
并注册,让我们将文件通过电子邮件直接发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持标签上。
-
点击代码下载和勘误表。
-
在搜索框中输入书名。
-
选择您希望下载代码文件的书籍。
-
从下拉菜单中选择您购买本书的渠道。
-
点击代码下载。
下载文件后,请确保使用最新版本的工具解压或提取文件夹:
-
Windows 版的 WinRAR / 7-Zip
-
Mac 版的 Zipeg / iZip / UnRarX
-
Linux 版的 7-Zip / PeaZip
完整的代码集也可以从以下 GitHub 仓库下载:github.com/PacktPublishing/Implementing-DevOps-on-AWS
。
下载本书的彩色图片
我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。这些彩色图片将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/ImplementingDevOpsonAWS_ColorImages.pdf
下载该文件。
勘误
尽管我们已尽力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们将非常感激您能向我们报告。通过这样做,您可以帮助其他读者避免困惑,并帮助我们改进后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表单链接,填写勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,勘误将被上传到我们的网站,或被添加到该书籍勘误列表中的“勘误”部分。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support
,并在搜索框中输入书名。所需信息将出现在勘误部分。
盗版
互联网版权盗版问题在各类媒体中一直存在。我们在 Packt 非常重视版权和许可证的保护。如果你在互联网上遇到任何非法复制的我们作品的形式,请立即提供该地址或网站名称,以便我们采取措施。
请通过链接将涉嫌盗版的资料发送至 copyright@packtpub.com。
我们感谢你在保护我们的作者及其作品内容方面的帮助。
问题
如果你对本书的任何内容有问题,可以通过 questions@packtpub.com 联系我们,我们将尽力解决问题。
第一章:什么是 DevOps,为什么你应该关心?
DevOps 可以看作是成功且成熟的敏捷(Agile)方法论的延伸。将运维纳入其中有助于从持续(敏捷)开发到集成再到部署的进步,但更重要的是,它有助于构建一个更好的工作环境,一个具有更强团队间合作关系的环境。
如果我只能用一个词来描述 DevOps,那就是“合作”。开发(Dev)和运维(Ops)阵营都愿意真正合作,这是其哲学的基础,也是最重要的方面。
DevOps 出现在以下图表中的交汇点:
本章将涵盖以下主题:
-
什么是 DevOps?
-
在采纳 DevOps 之前你应该问自己的问题
什么是 DevOps?
所以,让我们来探讨一下 DevOps 环境的一些基本特征。
接下来是一些普遍接受的定义,通常混合着个人意见——你已经被提醒了。
共同目标
将努力方向对准提高系统性能与稳定性、减少部署时间或提升产品整体质量,将使客户更满意,工程师们也会更自豪。
目标需要反复重复、澄清和简化,直到每个人都完全理解、提出反对意见,并最终达成共识。
DevOps 将焦点从个人利益转向共同目标。它将赞扬更多地放在团队成就上,而不是个人;KPI和本月员工之类的活动也许就不那么重要了。
让人们看到超越自己工位的小环境的大图景。相信他们。
共享知识(无孤岛)
你很可能已经听说过关于臭名昭著的组织孤岛的故事,或者读过相关书籍。
在最坏的情况下,可能会出现一个拒绝放手的人,他们往往成为开发生命周期中的主要瓶颈。他们可能非常占有欲强,守护着自己在某一领域中的专属知识,可能(我推测)是因为这让他们感到重要,进一步迎合了他们的自我。
另一方面,也有一些人由于不幸的情况而陷入孤岛。我对那些孤身一人支持遗留系统的工程师们深感敬意。
幸运的是,DevOps 通过像跨职能团队和全栈工程师这样的概念,模糊了这些专业领域的界限。这里需要注意的是,这并不意味着可以通过期望人们在每个领域都成为技术高手来削减成本(这在现实中相当于在平均水平之上)。但就像 Venn 图中的交集一样,它是开发(Dev)和运维(Ops)技能集合之间的交汇点。
通过鼓励知识共享,避免了信息孤岛。同行评审、演示站立会议或共享文档是确保没有任务或知识被局限于特定人的几种方式。你甚至可以采用结对编程。这似乎有点重,但显然是有效的!
信任与共享责任
是否应该给予开发人员生产环境访问权限?
维持严格的基于角色的权限管理是有充分理由的,其中之一是安全性,另一个是完整性。只要我们保持开发人员的刻板印象——他们习惯于在devlocal环境中工作;对他们来说,诸如受密码保护的SSH 密钥,或者不手动编辑所有文件等概念,常常被忽略。
在 DevOps 时代,情况已经不再如此。共享知识和责任意味着运维工程师可以依赖他们的开发同事,在关键的生产环境中遵循相同的行为准则。
开发和运维团队可以使用相同的工具和环境。部署不再是运维团队的特殊任务,也不再需要提前安排日期。
在一个拥有这种知识共享习惯的团队中,作为运维工程师的我可以对我的开发同事能够执行我的任务充满信心,反之亦然。
开发人员参与值班轮班,支持他们所开发的软件。
这不应被视为一种额外负担,而应视为信任的象征和增加协作的机会。现在没有人再把代码扔到墙那边了。责任感和自主性激励人们做出超出预期的贡献。
尊重
随着我们花更多时间相互交流面对的挑战和试图解决的问题,我们之间的相互尊重也在不断增加。
这表现在开发人员在软件开发过程的早期就寻求运维团队的意见,或是运维工具的构建是为了满足开发人员的需求。
像开发人员一样思考的运维人员,像运维人员一样思考的开发人员 | ||
---|---|---|
--John Allspaw 和 Paul Hammond,《Velocity》 |
DevOps 环境建立在这种尊重之上。这是一个每个意见都很重要的地方,在这里,人们可以并且确实会公开质疑决策,以求得问题的最佳解决方案。这是一个强有力的标志,表明个人对我之前提到的共同目标的承诺。
自动化
从 A. Maslow 的动机理论中得出一个过于简化的结论:当你饥饿时,你不太可能去考虑诗歌。换句话说,一个满足基本需求的团队会在解决基本问题。
自动化日常且单调的任务使工程师能够集中精力处理更复杂、更有价值的工作。此外,人们会感到无聊,开始偷工减料,犯错——而计算机则不太容易犯错。
可重现的基础设施
将基础设施描述为代码具有以下优势:
-
它可以通过版本控制进行管理。
-
它可以轻松地与他人共享,以供重用或复制。
-
它充当了一个非常有用的日记,记录了你做了什么以及你是如何做的。
-
配置云环境变得非常简单(例如,通过 Terraform 或 CloudFormation)。
-
它使现代配置管理成为可能
无论如何,我怀疑任何管理超过 10 台服务器的人,已经以某种方式在对基础设施进行编码了。
指标与监控
测量一切! | ||
---|---|---|
--实际的 DevOps 口号 |
存储变得便宜。培养收集大量度量数据的习惯,并使这些数据在你的组织中容易访问。工程师们对其基础设施和应用程序性能的可见性越高,在关键情况下做出的决策就越得当。
图表能够传达大量信息,在大屏幕上看起来相当酷,而且人类的大脑在识别模式方面表现出色。
指标数据的另一个重要作用是在性能优化中。
加速程序的最棘手部分不是做这件事,而是决定是否值得做这件事...问题的一部分在于,优化很难做好。令人害怕的是,它很容易变成迷信仪式和合理化的过程。 | ||
---|---|---|
--成熟的优化,Carlos Bueno |
为了避免陷入确认偏误,你需要在尝试任何优化之前和之后使用客观的方法来评估你的系统。使用这些指标;(有效的)数据是很难被反驳的。
在有效性问题上,请定期校准你的仪器,进行合理性检查输出,并确保你认为展示的内容是同事们认为看到的内容(参考:mars.jpl.nasa.gov/msp98/news/mco990930.html
)。
持续集成、交付和部署
观察、定位、决策和行动(OODA)循环是由 J·博伊德上校提出的概念,展示了一个人在不断变化的环境中适应能力的价值。
面对无情(而且富有成效)的竞争,组织应该能够迅速应对动态市场条件。
这可能通过老式的柯达和 Netflix 故事来最好地说明。前者在曾经获得巨大成功后,被认为未能适应其行业的新趋势,导致品牌逐渐消失。相比之下,Netflix 不断巧妙地调整他们的产品以适应我们消费数字内容的新方式。他们彻底改变了他们的基础设施,分享了一些聪明的新做法,甚至是一些有争议的实践,再加上一堆出色的软件。像 Netflix 一样。
持续集成与交付本质上就是 OODA 的实践。团队不断集成相对较小的代码更改,更频繁地交付版本,从而能更快地从用户那里获得反馈。这种反馈类型对组织在动态变化的市场中作出及时反应至关重要。
然而,以上并不意味着我们应该成为发布英雄,将一切仓促推向生产环境,每周两次点燃它。CI/CD框架依然意味着通常的严格代码审查和测试流程,不论你部署得多频繁。尽管代码审查和测试通常需要更少的时间和精力,因为部署频率越高,代码变动通常越小。
拥抱失败
自然地,更多的实验可能会增加错误的概率。
我怀疑这一事实对任何人都不构成惊讶;然而,可能让你感到惊讶的是,接受失败的另一个积极角度的建议。
回想一下前一节中的视频极客们。他们可不是轻松地度过所有这些变化的,没有一点代价。此处我就不引用爱迪生的名言了;然而,试错确实是科学方法的一种有效形式,而 DevOps 流程为那些愿意同意这一点的人提供了极大的助力。
换句话说,组织应该鼓励人们不断挑战并改善当前的现状,同时也要允许他们公开讨论发生错误时的经历。
但是,处理实验失败可能是这段故事中比日常操作中冷酷、严苛的现实更浪漫的一面。
系统会失败。我希望我们大多数人都已经接受了这一事实,并接受它引发的思维链条:
-
我们并不总是知道自己认为知道的那么多:
“对结果的了解使得事情似乎应该更明显地呈现给当时的实践者,然而这并非实际情况……**在事故发生后,实践者的行为可能会被视为错误或违反规定,但这些评估在很大程度上受到事后诸葛亮的偏见,忽视了其他推动因素……” --复杂系统如何失败,R.I.库克 向上攀登! 或者说,在我们长期追求社会主导地位的过程中,我们似乎已经形成了这样一个便捷的信念:事件发生后,我们不仅能准确知道它发生了什么、怎么发生的,还能知道为什么会发生。这种奇特的现象已经在 D.卡尼曼的《思考,快与慢》中得到了很好的解释;我只是想补充一点,确实经常听到一些过于自信的人,他们根据自己看似合理的故事线,指责同事。
事情的真相是:我们当时不在现场。而将我们现在知道的细节与当时已知的事实区分开来并不容易。
-
指责毫无价值:
我们社区中的 Etsy 及类似平台分享了足够多的观察结果,表明将负面强化作为减少人为错误的策略并不是最优选择。
采用 DevOps 后,我们接受人们通常每天上班时的目标是尽最大努力工作,并为组织的利益着想。在停机发生后,我们的分析从假设操作员在当时的情境和信息基础上做出了最好的决策开始。我们关注可能导致他们做出决策的因素、他们的思考过程、事件的流动,以及这些是否能得到改进。
-
韧性是可以积累的:
“使我们不死的……”mithridatism*或纳西姆·塔勒布的反脆弱概念都在表达这样一个观点:我们在遇到负面经历时变得更擅长应对,而且更重要的是,我们应该时不时地去主动寻找这些经历。
我们可以训练自己和我们的系统,更快速地从错误中恢复,甚至在错误发生时依然能够继续运行。实现这一目标的一种方式是通过有控制的(并且随着实践,变得不那么控制)停机。
有了合适的监控和审计工具,每一次异常活动都能让我们更深入地了解我们的应用程序和基础设施。
现在,我已将通过 DevOps 实现更好生活的秘密传授给你,亲爱的读者,接下来我们来关注本章标题的后半部分。
你在乎吗?
我看不出有什么理由不这样做。从 DevOps 概念诞生至今已过去七年左右,关于其有效性的证据也在稳步增长。以备受尊敬的敏捷框架为基础,更加增强了其可信度,也许能帮助解释其成功的很大一部分原因。
这并不是说没有需要考虑的事项。你内心的批判性思维者,在开始这种文化政变之前,肯定会问上几个问题。
现在是时候了吗?
你刚刚完成了精益或敏捷开发的实施吗?团队里还有其他变化吗?现在是时候再一次呼吁变革了吗?
改变我们的习惯会让我们感到不安;需要一段时间来调整。你的坚持是值得赞扬的,追求 DevOps 作为团队协作的新阶段往往是正确的选择。
不必完全放弃;或许可以暂时搁置一下。
它会有效吗?
环顾四周。那些面孔,那些不同的个性,你能想象他们一起唱《Kumbaya》吗?也许能,也许不能,或者还不能。
请不要通过电子邮件发送匿名员工调查。把大家聚在一起,展示你的 DevOps 宣传,并观察他们的反应。
你将需要每个人完全理解这些概念,承认挑战,并接受为此付出的牺牲。不能有任何例外,也不能有模糊不清之处。
所有这些都需要极大的文化变革,团队应对此做好准备。
这值得吗?
改变当前思维方式需要什么?你需要制造多大的扰动?你预期会有多大的反响?
虽然我并不是建议将此作为容忍现状的借口,但我恳请你保持一种务实的态度看待当前的情况。
你的组织类型可能更适合逐步演变的过程,而不是一次彻底的革命。
你需要它吗?
你如何评估你目前的流程?你会说你的跨团队沟通令人满意吗?你是否经常达成业务期望?你是否已经自动化了大部分工作流程?
听起来你目前做得很好;你可能已经在团队中有一些 DevOps 实践,只是没有意识到而已。关键是,如果你能将资源集中在其他地方进行优化,解决其他更紧迫的问题,可能会更有效。
既然你已经了解了 DevOps 背后思想的另一种诠释,如果你觉得这些观点与你的思维方式相符,并且最后几个问题没有引发任何疑虑,那么我们可以安全地过渡到更具技术性的主题,将原则付诸实践。
总结
首先,我们探讨了 DevOps 哲学中包含的主要理念,接着提出了一些问题,旨在帮助你在组织内采纳 DevOps 时构建一个更客观的视角。
我们已经看到,DevOps 是一些老旧且经过验证的敏捷概念与其他较新发展出的理念的有效结合,它教会我们如何建立更好的团队,写出更好的软件,获得更快的成果,并在一个鼓励实验而不妥协稳定性的环境中轻松合作。
既然我们已经覆盖了理论内容,接下来的章节将引导我们进入 DevOps 的实际应用。我们将从在云端部署基础设施作为代码的示例开始。
第二章:开始将基础设施视为代码
诸位,举起你们的手,程序化基础设施来了!
也许基础设施即代码(IaC)并不是一个全新的概念,因为配置管理已经存在了很长时间。然而,将服务器、存储和网络基础设施及其关系编写成代码的做法,是云计算崛起后才出现的相对较新的趋势。不过,让我们先放下配置管理,集中注意力在 IaC 的第二个方面。
你应该记得上一章中存储所有内容为代码的一些好处:
-
代码可以保持版本控制
-
代码可以轻松共享和协作
-
代码即文档
-
代码是可复现的
最后一点对我个人来说是一个巨大的收获。自动化资源配置帮助将部署一个功能齐全的云环境的时间从四小时减少到一个小时,并且将人为错误的发生率降到了几乎为零(不能让一个人负责输入字段)。
当团队开始并行使用多个环境并且需要随时启动或停止这些环境时,能够快速配置资源成为一个显著的优势。在这一章节中,我们将详细探讨如何用代码描述并在 AWS 上部署这样的一个环境,并尽量减少手动操作。
为了在云中实现 IaC,我们将关注两个工具或服务:Terraform和CloudFormation。
我们将通过以下示例进行讲解:
-
配置工具
-
编写 IaC 模板
-
部署模板
-
部署模板的后续变更
-
删除模板并移除已配置的基础设施
在这些示例中,假设我们的应用程序需要一个虚拟私有云(VPC),它托管一个关系型数据库服务(RDS)后端和若干个在弹性负载均衡器(ELB)后面的弹性计算云(EC2)实例。我们将大多数组件置于网络地址转换(NAT)背后,只允许负载均衡器可以从外部访问。
使用 Terraform 实现基础设施即代码(IaC)
部署 AWS 基础设施的工具之一是 HashiCorp 的 Terraform(www.terraform.io
)。HashiCorp 是那个给我们带来 Vagrant、Packer 和 Consul 的天才团队。如果你还没看过他们的网站,我推荐你去看看。
使用Terraform(TF),我们可以编写一个模板来描述一个环境,进行模拟运行查看即将发生的事情以及是否符合预期,部署模板,并在必要时进行任何后期调整——这一切都无需离开终端。
配置
首先,你需要在你的机器上安装一份 TF(www.terraform.io/downloads.html
),并确保它能在 CLI 中使用。你应该能够查询到当前安装的版本,在我这里是 0.6.15:
$ terraform --version
Terraform v0.6.15
由于 TF 使用 AWS API,它需要一组认证密钥以及一定的 AWS 账户访问权限。为了部署本章中的示例,您可以创建一个新的 身份 和 访问管理(IAM)用户,并授予以下权限:
"autoscaling:CreateAutoScalingGroup",
"autoscaling:CreateLaunchConfiguration",
"autoscaling:DeleteLaunchConfiguration",
"autoscaling:Describe*",
"autoscaling:UpdateAutoScalingGroup",
"ec2:AllocateAddress",
"ec2:AssociateAddress",
"ec2:AssociateRouteTable",
"ec2:AttachInternetGateway",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CreateInternetGateway",
"ec2:CreateNatGateway",
"ec2:CreateRoute",
"ec2:CreateRouteTable",
"ec2:CreateSecurityGroup",
"ec2:CreateSubnet",
"ec2:CreateTags",
"ec2:CreateVpc",
"ec2:Describe*",
"ec2:ModifySubnetAttribute",
"ec2:RevokeSecurityGroupEgress",
"elasticloadbalancing:AddTags",
"elasticloadbalancing:ApplySecurityGroupsToLoadBalancer",
"elasticloadbalancing:AttachLoadBalancerToSubnets",
"elasticloadbalancing:CreateLoadBalancer",
"elasticloadbalancing:CreateLoadBalancerListeners",
"elasticloadbalancing:Describe*",
"elasticloadbalancing:ModifyLoadBalancerAttributes",
"rds:CreateDBInstance",
"rds:CreateDBSubnetGroup",
"rds:Describe*"
注意
请参阅此文件以获取更多信息:github.com/PacktPublishing/Implementing-DevOps-on-AWS/blob/master/5585_02_CodeFiles/Terraform/iam_user_policy.json
。
使 IAM 用户的凭证可供 TF 使用的一种方法是通过导出以下环境变量:
$ export AWS_ACCESS_KEY_ID='user_access_key'
$ export AWS_SECRET_ACCESS_KEY='user_secret_access_key'
这应该足以让我们开始了。
注意
下载示例代码
下载代码包的详细步骤已在本书的前言中提到。
本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Implementing-DevOps-on-AWS
。我们还有来自我们丰富书籍和视频目录的其他代码包,可以在:github.com/PacktPublishing/
中查看。快去看看吧!
模板设计
在我们开始编码之前,这里有一些规则:
-
您可以选择将 TF 模板编写为一个大的文件,或多个小文件的组合
-
模板可以用纯 JSON 或 TF 自有格式编写
-
TF 会在给定的文件夹中查找扩展名为
.tf
或.tf.json
的文件,并按字母顺序加载它们。 -
TF 模板是声明式的,因此资源在模板中出现的顺序不会影响执行流程
TF 模板通常由三部分组成:资源、变量 和 输出。如前所述,如何排列这些部分是个人偏好问题;然而,为了更好的可读性,我建议我们使用 TF 格式,并将每个部分写入单独的文件中。此外,尽管文件扩展名很重要,文件名则由你决定。
资源
从某种程度上讲,这个文件包含了模板的主要部分,因为资源代表了最终被配置的实际组件。例如,我们将使用 VPC Terraform 资源、RDS、ELB 以及其他几个资源来配置大致如下所示的内容:
由于模板元素可以按任何顺序编写,TF 通过检查它找到的任何引用来确定执行流程(例如,VPC 应该在 ELB 被创建之前存在,ELB 被认为属于它)。另外,也可以使用显式的流程控制属性,例如 depends_on
,正如我们稍后会看到的那样。
为了了解更多信息,让我们浏览一下 resources.tf
文件的内容。
注意
首先,我们告诉 Terraform 使用哪个提供程序来部署我们的基础设施:
# Set a Provider
provider "aws"
{
region = "${var.aws-region}"
}
你会注意到没有指定凭证,因为我们之前已经将它们设置为环境变量。
现在我们可以添加 VPC 及其网络组件:
# Create a VPC
resource "aws_vpc" "terraform-vpc"
{
cidr_block = "${var.vpc-cidr}"
tags
{
Name = "${var.vpc-name}"
}
}
# Create an Internet Gateway
resource "aws_internet_gateway" "terraform-igw"
{
vpc_id = "${aws_vpc.terraform-vpc.id}"
}
# Create NAT
resource "aws_eip" "nat-eip"
{
vpc = true
}
到目前为止,我们已经声明了 VPC、其互联网和 NAT 网关,以及一组具有匹配路由表的公有和私有子网。
如果我们逐行查看一些资源块,它有助于澄清语法:
resource "aws_subnet" "public-1" {
第一个参数是资源的类型,后面跟着一个任意的名称:
vpc_id = "${aws_vpc.terraform-vpc.id}"
名为public-1
的aws_subnet
资源具有一个vpc_id
属性,它引用了名为terraform-vpc
的另一个aws_vpc
资源的id
属性。对其他资源的这种引用隐式地定义了执行流程,也就是说,VPC 必须先存在,才能创建子网:
cidr_block = "${cidrsubnet(var.vpc-cidr, 8, 1)}"
我们稍后会更多地讨论变量,但格式是var.var_name
,如这里所示。
在这里,我们使用cidrsubnet
函数和vpc-cidr
变量,它返回一个cidr_block
,将分配给public-1
子网。有关此函数和其他有用函数的详细信息,请参阅 TF 文档。
接下来,我们将 RDS 添加到 VPC 中:
resource "aws_db_instance" "terraform" {
identifier = "${var.rds-identifier}"
allocated_storage = "${var.rds-storage-size}"
storage_type= "${var.rds-storage-type}"
engine = "${var.rds-engine}"
engine_version = "${var.rds-engine-version}"
instance_class = "${var.rds-instance-class}"
username = "${var.rds-username}"
password = "${var.rds-password}"
port = "${var.rds-port}"
vpc_security_group_ids = ["${aws_security_group.terraform-rds.id}"]
db_subnet_group_name = "${aws_db_subnet_group.rds.id}"
}
在这里,我们主要看到对变量的引用,并且有一些对其他资源的调用。
在 RDS 之后是一个 ELB:
resource "aws_elb" "terraform-elb"
{
name = "terraform-elb"
security_groups = ["${aws_security_group.terraform-elb.id}"]
subnets = ["${aws_subnet.public-1.id}",
"${aws_subnet.public-2.id}"]
listener
{
instance_port = 80
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
tags
{
Name = "terraform-elb"
}
}
最后,我们定义了 EC2 自动扩展组和相关资源,如启动配置。
对于启动配置,我们定义了要使用的 AMI 和实例类型,SSH 密钥对的名称,EC2 安全组以及用于引导实例的 UserData:
resource "aws_launch_configuration" "terraform-lcfg" {
image_id = "${var.autoscaling-group-image-id}"
instance_type = "${var.autoscaling-group-instance-type}"
key_name = "${var.autoscaling-group-key-name}"
security_groups = ["${aws_security_group.terraform-ec2.id}"]
user_data = "#!/bin/bash \n set -euf -o pipefail \n exec 1> >(logger -s -t $(basename $0)) 2>&1 \n yum -y install nginx; chkconfig nginx on; service nginx start"
lifecycle {
create_before_destroy = true
}
自动扩展组接收启动配置的 ID、VPC 子网列表、实例的最小/最大数量以及附加到已配置实例的 ELB 名称:
}
resource "aws_autoscaling_group" "terraform-asg" {
name = "terraform"
launch_configuration = "${aws_launch_configuration.terraform-lcfg.id}"
vpc_zone_identifier = ["${aws_subnet.private-1.id}", "${aws_subnet.private-2.id}"]
min_size = "${var.autoscaling-group-minsize}"
max_size = "${var.autoscaling-group-maxsize}"
load_balancers = ["${aws_elb.terraform-elb.name}"]
depends_on = ["aws_db_instance.terraform"]
tag {
key = "Name"
value = "terraform"
propagate_at_launch = true
}
}
前面的user_data
shell 脚本将在 EC2 节点上安装并启动 NGINX。
变量
我们充分利用变量来定义我们的资源,使模板尽可能可重用。现在,让我们看看variables.tf
,进一步研究这些变量。
与资源列表类似,我们从 VPC 开始:
注意
variable "aws-region" {
type = "string"
description = "AWS region"
}
variable "aws-availability-zones" {
type = "string"
description = "AWS zones"
}
variable "vpc-cidr" {
type = "string"
description = "VPC CIDR"
}
variable "vpc-name" {
type = "string"
description = "VPC name"
}
语法如下:
variable "variable_name" {
variable properties
}
variable_name
是任意的,但需要与模板中其他部分引用的相关var.var_name
匹配。例如,aws-region
变量将满足我们在描述provider aws resource
区域时使用的${var.aws-region}
引用。
我们大多数时候会使用 string
类型变量,但还有另一种有用的类型叫做 map,它可以存储查找表。Map 查询的方式类似于在哈希/字典中查找值(请参见:www.terraform.io/docs/configuration/variables.html
)。
接下来是 RDS:
variable "rds-identifier" {
type = "string"
description = "RDS instance identifier"
}
variable "rds-storage-size" {
type = "string"
description = "Storage size in GB"
}
variable "rds-storage-type" {
type = "string"
description = "Storage type"
}
variable "rds-engine" {
type = "string"
description = "RDS type"
}
variable "rds-engine-version" {
type = "string"
description = "RDS version"
}
variable "rds-instance-class" {
type = "string"
description = "RDS instance class"
}
variable "rds-username" {
type = "string"
description = "RDS username"
}
variable "rds-password" {
type = "string"
description = "RDS password"
}
variable "rds-port" {
type = "string"
description = "RDS port number"
}
最后,我们添加与 EC2 相关的变量:
variable "autoscaling-group-minsize" {
type = "string"
description = "Min size of the ASG"
}
variable "autoscaling-group-maxsize" {
type = "string"
description = "Max size of the ASG"
}
variable "autoscaling-group-image-id" {
type="string"
description = "EC2 AMI identifier"
}
variable "autoscaling-group-instance-type" {
type = "string"
description = "EC2 instance type"
}
variable "autoscaling-group-key-name" {
type = "string"
description = "EC2 ssh key name"
}
现在我们已经在 variables.tf
中定义了所有变量的类型和描述,但尚未分配值。
TF 在这方面非常灵活。我们可以采用以下任何方式:
-
在
variables.tf
中直接分配(默认)值: -
变量 "
aws-region
" {type = "string"``description = "AWS 区域"``default = 'us-east-1'
} -
不为变量分配值,这样 TF 会在运行时提示输入值
-
*
直接向 TF 命令传递-var 'key=value'
参数,如下所示:
-var 'aws-region=us-east-1'
-
将
key=value
键值对存储在文件中 -
使用以
TF_VAR
为前缀的环境变量,例如TF_VAR_aws-region
使用 key=value
键值对文件在团队内非常方便,因为每个工程师可以拥有一个私人副本(排除在版本控制之外)。如果文件名为 terraform.tfvars
,TF 会自动读取它;另外,也可以在命令行上使用 -var-file
来指定不同的源文件。
以下是我们示例 terraform.tfvars
文件的内容:
注意
autoscaling-group-image-id = "ami-08111162"
autoscaling-group-instance-type = "t2.nano"
autoscaling-group-key-name = "terraform"
autoscaling-group-maxsize = "1"
autoscaling-group-minsize = "1"
aws-availability-zones = "us-east-1b,us-east-1c"
aws-region = "us-east-1"
rds-engine = "postgres"
rds-engine-version = "9.5.2"
rds-identifier = "terraform-rds"
rds-instance-class = "db.t2.micro"
rds-port = "5432"
rds-storage-size = "5"
rds-storage-type = "gp2"
rds-username = "dbroot"
rds-password = "donotusethispassword"
vpc-cidr = "10.0.0.0/16"
vpc-name = "Terraform"
一个值得注意的点是 aws-availability-zones
,因为它包含多个值,我们通过元素和拆分函数与之进行交互,如在 resources.tf
中所示。
输出
我们模板中的第三部分,主要是提供信息的部分,包含 TF 输出。这些输出允许在测试、部署或模板部署后将选定的值返回给用户。这个概念类似于在 shell 脚本中常用的 echo
语句,用于在执行过程中显示有用的信息。
让我们通过创建一个 outputs.tf
文件来向模板中添加输出:
注意
output "VPC ID" {
value = "${aws_vpc.terraform-vpc.id}"
}
output "NAT EIP" {
value = "${aws_nat_gateway.terraform-nat.public_ip}"
}
output "ELB URI" {
value = "${aws_elb.terraform-elb.dns_name}"
}
output "RDS Endpoint" {
value = "${aws_db_instance.terraform.endpoint}"
}
要配置输出,您只需引用给定资源及其属性。如前面的代码所示,我们选择了 VPC 的 ID、NAT 网关的弹性 IP 地址、ELB 的 DNS 名称和 RDS 实例的端点地址。
本节完成了本示例中的模板。现在,您应该在模板文件夹中有四个文件:resources.tf
、variables.tf
、terraform.tfvars
和 outputs.tf
。
操作
我们将检查五个主要的 TF 操作:
-
验证模板
-
测试(干运行)
-
初始部署
-
更新部署
-
删除一个部署
注意
在以下命令行示例中,Terraform 在包含模板文件的文件夹中运行。
验证
在进一步操作之前,应使用 terraform validate
命令进行基本的语法检查。在 resources.tf
中重命名一个变量后,validate 会返回一个 unknown variable
错误:
$ terraform validate
Error validating: 1 error(s) occurred:
* provider config 'aws': unknown variable referenced: 'aws-region-1'. define it with 'variable' blocks
一旦变量名被修正,重新运行validate
命令将不会输出任何内容,这意味着验证已通过。
干运行
下一步是执行一个测试/干运行,使用 terraform plan
,它显示了实际部署过程中会发生的情况。该命令返回一个带有颜色编码的资源及其属性列表,或更准确地说,如下所示:
$ terraform plan
Resources are shown in alphabetical order for quick scanning. Green resources will be created (or destroyed and then created if an existing resource exists), yellow resources are being changed in-place, and red resources will be destroyed.
要真实地了解将要部署的基础设施的样子,你可以使用 terraform graph
:
$ terraform graph > my_graph.dot
DOT 文件可以通过 Graphviz 开源软件进行操作(请参见 www.graphviz.org
)或许多在线读取器/转换器。以下图示是一个较大图表的一部分,表示我们之前设计的模板:
Terraform 图表
部署
如果你对计划和图表满意,模板现在可以通过 terraform apply
部署:
$ terraform apply
aws_eip.nat-eip: Creating...
allocation_id: "" => "<computed>"
association_id: "" => "<computed>"
domain: "" => "<computed>"
instance: "" => "<computed>"
network_interface: "" => "<computed>"
private_ip: "" => "<computed>"
public_ip: "" => "<computed>"
vpc: "" => "1"
aws_vpc.terraform-vpc: Creating...
cidr_block: "" => "10.0.0.0/16"
default_network_acl_id: "" => "<computed>"
default_security_group_id: "" => "<computed>"
dhcp_options_id: "" => "<computed>"
enable_classiclink: "" => "<computed>"
enable_dns_hostnames: "" => "<computed>"
Apply complete! Resources: 22 added, 0 changed, 0 destroyed.
你的基础设施状态已保存到以下路径。这个状态对于修改和销毁你的基础设施是必需的,因此请妥善保管它。要查看完整的状态,请使用 terraform show
命令。
State path: terraform.tfstate
Outputs:
ELB URI = terraform-elb-xxxxxx.us-east-1.elb.amazonaws.com
NAT EIP = x.x.x.x
RDS Endpoint = terraform-rds.xxxxxx.us-east-1.rds.amazonaws.com:5432
VPC ID = vpc-xxxxxx
在成功部署结束时,你会看到之前配置的Outputs
以及关于另一个重要部分TF - 状态文件的信息(请参见 www.terraform.io/docs/state/
):
TF 存储的是上次运行 TF 时管理的基础设施的状态。默认情况下,这个状态存储在一个名为terraform.tfstate
的本地文件中,但它也可以远程存储,这在团队环境中效果更佳。
TF 使用此本地状态来创建计划并对你的基础设施进行更改。在任何操作之前,TF 会执行刷新,以用实际基础设施更新状态。
从某种意义上讲,state
文件包含了你基础设施的快照,并用于在模板被修改时计算任何更改。通常,你会将 terraform.tfstate
文件与模板一起纳入版本控制。然而,在团队环境中,如果遇到过多的合并冲突,你可以选择将 state
文件存储在如 S3 这样的替代位置(请参见:www.terraform.io/docs/state/remote/index.html
)。
让 EC2 节点完全初始化几分钟,然后尝试从前面的 Outputs
中加载 ELB URI 到浏览器中。你应该会看到 nginx,如下图所示:
更新
根据墨菲定律,一旦我们部署了一个模板,就会需要对其进行更改。幸运的是,所需的仅仅是更新并重新部署该模板。
假设我们需要向 ELB 安全组添加一个新规则(加粗部分显示):
-
更新
resources.tf
中的"aws_security_group" "terraform-elb"
资源块:resource "aws_security_group" "terraform-elb" { name = "terraform-elb" description = "ELB security group" vpc_id = "${aws_vpc.terraform-vpc.id}" ingress { from_port = "80" to_port = "80" protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = "443" to_port = "443" protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
-
验证即将更改的内容:
$ terraform plan ... ~ aws_security_group.terraform-elb ingress.#: "1" => "2" ingress.2214680975.cidr_blocks.#: "1" => "1" ingress.2214680975.cidr_blocks.0: "0.0.0.0/0" => "0.0.0.0/0" ingress.2214680975.from_port: "80" => "80" ingress.2214680975.protocol: "tcp" => "tcp" ingress.2214680975.security_groups.#: "0" => "0" ingress.2214680975.self: "0" => "0" ingress.2214680975.to_port: "80" => "80" ingress.2617001939.cidr_blocks.#: "0" => "1" ingress.2617001939.cidr_blocks.0: "" => "0.0.0.0/0" ingress.2617001939.from_port: "" => "443" ingress.2617001939.protocol: "" => "tcp" ingress.2617001939.security_groups.#: "0" => "0" ingress.2617001939.self: "" => "0" ingress.2617001939.to_port: "" => "443" Plan: 0 to add, 1 to change, 0 to destroy.
-
部署更改:
$ terraform apply ... aws_security_group.terraform-elb: Modifying... ingress.#: "1" => "2" ingress.2214680975.cidr_blocks.#: "1" => "1" ingress.2214680975.cidr_blocks.0: "0.0.0.0/0" => "0.0.0.0/0" ingress.2214680975.from_port: "80" => "80" ingress.2214680975.protocol: "tcp" => "tcp" ingress.2214680975.security_groups.#: "0" => "0" ingress.2214680975.self: "0" => "0" ingress.2214680975.to_port: "80" => "80" ingress.2617001939.cidr_blocks.#: "0" => "1" ingress.2617001939.cidr_blocks.0: "" => "0.0.0.0/0" ingress.2617001939.from_port: "" => "443" ingress.2617001939.protocol: "" => "tcp" ingress.2617001939.security_groups.#: "0" => "0" ingress.2617001939.self: "" => "0" ingress.2617001939.to_port: "" => "443" aws_security_group.terraform-elb: Modifications complete ... Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
注意
一些更新操作可能是破坏性的(请参见
docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html
)。你应该始终查看 CloudFormation 文档,了解你计划修改的资源是否会导致中断。TF 通过prevent_destroy
生命周期属性提供了一些保护(请参见www.terraform.io/docs/configuration/resources.html#prevent_destroy
)。
移除
这是一个友好的提醒:完成实验后,务必移除 AWS 资源,以避免产生意外费用。
在执行任何delete
操作之前,我们需要授予之前在本章开始时创建的(terraform
) IAM 用户相关权限。作为快捷方式,你可以通过 AWS 控制台临时将AdministratorAccess托管策略附加到该用户,如下图所示:
为了移除我们在本示例中创建的 VPC 及其所有相关资源,我们将使用terraform destroy
:
$ terraform destroy
Do you really want to destroy?
Terraform will delete all your managed infrastructure.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
Terraform
会请求确认后继续销毁资源,最终显示如下:
Apply complete! Resources: 0 added, 0 changed, 22 destroyed.
接下来,我们通过卸载AdministratorAccess托管策略来移除之前授予 IAM 用户的临时管理员访问权限,如下图所示:
然后,验证 VPC 是否已在 AWS 控制台中消失。
使用 CloudFormation 进行基础设施即代码(IaC)
CloudFormation是 AWS 提供的一项基础设施即代码(IaC)服务。和之前一样,我们将通过包含参数(变量)、资源和输出的模板来描述我们的基础设施。
CloudFormation 将每个部署的模板称为Stack。可以通过 AWS 控制台、CLI 或 API 创建、列出、更新和删除 Stack。在小型设置中,你可能会单独部署每个 Stack,但随着架构变得更复杂,你可以开始嵌套 Stack。你将有一个顶级或父级 Stack(模板),它调用多个子 Stack。嵌套 Stack 允许你在它们之间传递变量,并且当然能节省你单独部署每个 Stack 的时间。
配置
CloudFormation 通过 AWS 控制台提供 GUI 界面;然而,我们将重点关注 AWS CLI,因为它更适合未来的自动化任务。
根据你使用的操作系统,你可以从 aws.amazon.com/cli/
下载安装程序,或者使用 Python PIP:
$ pip install awscli
$ aws --version
aws-cli/1.10.34 ...
我们将需要一组 API 密钥,因此让我们创建一个名为 cloudformation
的新 IAM 用户,并赋予以下权限:
"cloudformation:CancelUpdateStack",
"cloudformation:ContinueUpdateRollback",
"cloudformation:Create*",
"cloudformation:Describe*",
"cloudformation:EstimateTemplateCost",
"cloudformation:ExecuteChangeSet",
"cloudformation:Get*",
"cloudformation:List*",
"cloudformation:PreviewStackUpdate",
"cloudformation:SetStackPolicy",
"cloudformation:SignalResource",
"cloudformation:UpdateStack",
"cloudformation:ValidateTemplate",
"autoscaling:CreateAutoScalingGroup",
"autoscaling:CreateLaunchConfiguration",
"autoscaling:DeleteLaunchConfiguration",
"autoscaling:Describe*",
"autoscaling:UpdateAutoScalingGroup",
"ec2:AllocateAddress",
"ec2:AssociateAddress",
"ec2:AssociateRouteTable",
"ec2:AttachInternetGateway",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CreateInternetGateway",
"ec2:CreateNatGateway",
"ec2:CreateRoute",
"ec2:CreateRouteTable",
"ec2:CreateSecurityGroup",
"ec2:CreateSubnet",
"ec2:CreateTags",
"ec2:CreateVpc",
"ec2:Describe*",
"ec2:Modify*",
"ec2:RevokeSecurityGroupEgress",
"elasticloadbalancing:CreateLoadBalancer",
"elasticloadbalancing:CreateLoadBalancerListeners",
"elasticloadbalancing:Describe*",
"elasticloadbalancing:ModifyLoadBalancerAttributes",
"elasticloadbalancing:SetLoadBalancerPoliciesOfListener",
"rds:CreateDBInstance",
"rds:CreateDBSubnetGroup",
"rds:Describe*"
注意
你可以选择使用 aws configure
,它会提示你输入 API 凭证,或者如果你不想永久存储它们,你也可以使用环境变量:
$ export AWS_ACCESS_KEY_ID='user_access_key'
$ export AWS_SECRET_ACCESS_KEY='user_secret_access_key'
CloudFormation 模板不会存储任何 AWS 区域信息,因此为了避免每次在命令行中指定,可以将其导出:
$ export AWS_DEFAULT_REGION='us-east-1'
配置好这些环境变量后,awscli
应该可以开始使用了。
模板设计
CloudFormation 模板是用 JSON 编写的,通常至少包含三个部分(顺序无关):参数、资源和输出。
不幸的是,无法将这些参数存储到单独的文件中(参数值除外),因此在这个例子中,我们将使用一个名为main.json
的单一模板文件。
模板可以在本地使用,也可以从远程位置导入(S3 存储桶是常见的选择)。
参数
参数通过允许我们传递变量(例如实例类型、AMI ID、SSH 密钥对名称等)为我们的堆栈增加了灵活性和可移植性,这些值最好不要硬编码。
每个参数都有一个任意的逻辑名称(字母数字,模板中唯一)、描述、类型和一个可选的默认值。可用的类型包括 String
、Number
、CommaDelimitedList
,以及一些特定于 AWS 的特殊类型,例如 AWS::EC2::KeyPair::KeyName
,如前面的代码所示。
后者对于验证很有用,因为 CloudFormation 会检查给定名称的密钥对是否确实存在于你的 AWS 账户中。
参数还可以具有诸如 AllowedValues
、Min/MaxLength
、Min/MaxValue
、NoEcho
等属性(请参阅 docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
)。
每个模板的参数限制为 60 个。
让我们来检查一下模板顶部的参数:
注意
"Parameters" : {
"vpcCidr" : {
"Description" : "VPC CIDR",
"Type" : "String"
},
"vpcName" : {
"Description" : "VPC name",
"Type" : "String"
},
"awsAvailabilityZones" : {
"Description" : "List of AZs",
"Type" : "CommaDelimitedList"
},
"publicCidr" : {
"Description" : "List of public subnet CIDRs",
"Type" : "CommaDelimitedList"
},...
"rdsInstanceClass" : {
"Description" : "RDS instance class",
"Type" : "String",
"AllowedValues" : ["db.t2.micro", "db.t2.small", "db.t2.medium"]
},
"rdsUsername" : {
"Description" : "RDS username",
"Type" : "String"
},
"rdsPassword" : {
"Description" : "RDS password",
"Type" : "String",
"NoEcho" : "true"
},
...
"autoscalingGroupKeyname" : {
"Description" : "EC2 ssh key name",
"Type" : "AWS::EC2::KeyPair::KeyName"
},
"autoscalingGroupImageId" : {
"Description" : "EC2 AMI ID",
"Type" : "AWS::EC2::Image::Id"
}
}
我们使用了以下内容:
-
CommaDelimitedList
,我们稍后会通过一个特殊的函数方便地查询它 -
使用
AllowedValues
和MinValue
来强制执行约束 -
NoEcho
用于密码或其他敏感数据 -
一些 AWS 特定的类型用于进一步验证输入
你会注意到,前面的参数没有被赋予任何值。
为了维护可重用的模板,我们将值存储在一个单独的文件中(parameters.json
):
注意
[
{
"ParameterKey": "vpcCidr",
"ParameterValue": "10.0.0.0/16"
},
{
"ParameterKey": "vpcName",
"ParameterValue": "CloudFormation"
},
{
"ParameterKey": "awsAvailabilityZones",
"ParameterValue": "us-east-1b,us-east-1c"
},
{
"ParameterKey": "publicCidr",
"ParameterValue": "10.0.1.0/24,10.0.3.0/24"
},
{
"ParameterKey": "privateCidr",
"ParameterValue": "10.0.2.0/24,10.0.4.0/24"
},
{
"ParameterKey": "rdsIdentifier",
"ParameterValue": "cloudformation"
},
{
"ParameterKey": "rdsStorageSize",
"ParameterValue": "5"
},
{
"ParameterKey": "rdsStorageType",
"ParameterValue": "gp2"
},
{
"ParameterKey": "rdsEngine",
"ParameterValue": "postgres"
},...
资源
你已经熟悉资源的概念,以及它们如何用于描述不同的基础设施组件。
无论资源在模板中的出现顺序如何,CloudFormation 都会遵循其内部逻辑来决定这些资源的配置顺序。
声明资源的语法如下:
"Logical ID" : {
"Type" : "",
"Properties" : {}
}
ID 需要是字母数字组合,并且在模板内是唯一的。
CloudFormation 资源类型及其属性的列表可以在这里找到:docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
一个模板中最多可以有 200 个资源。达到该限制后,你需要将模板拆分成更小的模板,并可能需要考虑使用嵌套堆栈。
回到我们的例子,按照传统,我们首先创建一个 VPC 及其支持元素,如子网、互联网网关和 NAT 网关:
注意
"Resources" : {
"vpc" : {
"Type" : "AWS::EC2::VPC",
"Properties" : {
"CidrBlock" : { "Ref" : "vpcCidr" },
"EnableDnsSupport" : "true",
"EnableDnsHostnames" : "true",
"Tags" : [ { "Key" : "Name", "Value" : { "Ref" : "vpcName" } } ]
}
},
"publicSubnet1" : {
"Type" : "AWS::EC2::Subnet",
"Properties" : {
"AvailabilityZone" : { "Fn::Select" : [ "0", {"Ref" : "awsAvailabilityZones"} ] },
"CidrBlock" : { "Fn::Select" : [ "0", {"Ref" : "publicCidr"} ] },
"MapPublicIpOnLaunch" : "true",
"Tags" : [ { "Key" : "Name", "Value" : "Public" } ],
"VpcId" : { "Ref" : "vpc" }
}
},
...
"internetGateway" : {
"Type" : "AWS::EC2::InternetGateway",
"Properties" : {
"Tags" : [ { "Key" : "Name", "Value" : { "Fn::Join" : [ " - ", [ { "Ref" : "vpcName" }, "IGW" ] ] } } ]
}
},
"internetGatewayAttachment" : {
"Type" : "AWS::EC2::VPCGatewayAttachment",
"Properties" : {
"InternetGatewayId" : { "Ref" : "internetGateway" },
"VpcId" : { "Ref" : "vpc" }
}
},
"natEip" : {
"Type" : "AWS::EC2::EIP",
"Properties" : {
"Domain" : "vpc"
}
},
"natGateway" : {
"Type" : "AWS::EC2::NatGateway",
"Properties" : {
"AllocationId" : { "Fn::GetAtt" : ["natEip", "AllocationId"]},
"SubnetId" : { "Ref" : "publicSubnet1" }
},
"DependsOn" : "internetGatewayAttachment"
},
请注意,前面代码中使用的一些CloudFormation
函数:
-
"Fn::Select"
在"CidrBlock" : { "Fn::Select" : [ "0", {"Ref" : "publicCidr"} ] }
中使用,允许我们查询之前设置的CommaDelimitedList
类型的参数。 -
"Fn::Join"
,用于连接字符串 -
"Fn::GetAtt"
,用于获取资源属性
此外,natGateway
资源的DependsOn
属性允许我们对执行顺序设置明确的条件。在这种情况下,我们声明互联网网关资源需要准备好(已附加到 VPC)后,才会配置 NAT 网关。
在创建 VPC 之后,我们添加 RDS:
"rdsInstance" : {
"Type" : "AWS::RDS::DBInstance",
"Properties" : {
"DBInstanceIdentifier" : { "Ref" : "rdsIdentifier" },
"DBInstanceClass" : { "Ref" : "rdsInstanceClass" },
"DBSubnetGroupName" : { "Ref" : "rdsSubnetGroup" },
"Engine" : { "Ref" : "rdsEngine" },
"EngineVersion" : { "Ref" : "rdsEngineVersion" },
"MasterUserPassword" : { "Ref" : "rdsPassword" },
"MasterUsername" : { "Ref" : "rdsUsername" },
"StorageType" : { "Ref" : "rdsStorageType" },
"AllocatedStorage" : { "Ref" : "rdsStorageSize" },
"VPCSecurityGroups" : [ { "Ref" : "rdsSecurityGroup" } ],
"Tags" : [ { "Key" : "Name", "Value" : { "Ref" : "rdsIdentifier" } } ]
}}
然后添加 ELB:
...
"elbInstance" : {
"Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
"Properties" : {
"LoadBalancerName" : "cloudformation-elb",
"Listeners" : [ { "InstancePort" : "80", "InstanceProtocol" : "HTTP", "LoadBalancerPort" : "80", "Protocol" : "HTTP" } ],
"SecurityGroups" : [ { "Ref" : "elbSecurityGroup" } ],
"Subnets" : [ { "Ref" : "publicSubnet1" }, { "Ref" : "publicSubnet2" } ],
"Tags" : [ { "Key" : "Name", "Value" : "cloudformation-elb" } ]
}
}
然后添加 EC2 资源:
...
"launchConfiguration" : {
"Type" : "AWS::AutoScaling::LaunchConfiguration",
"Properties" : {
"ImageId" : { "Ref": "autoscalingGroupImageId" },
"InstanceType" : { "Ref" : "autoscalingGroupInstanceType" },
"KeyName" : { "Ref" : "autoscalingGroupKeyname" },
"SecurityGroups" : [ { "Ref" : "ec2SecurityGroup" } ]
我们仍然使用UserData
脚本来安装 NGINX 包;然而,这次展示的方式略有不同。CloudFormation
将使用换行符作为分隔符连接这些行,然后将结果编码为Base64
格式:
"UserData" : {
"Fn::Base64" : {
"Fn::Join" : [
"\n",
[
"#!/bin/bash",
"set -euf -o pipefail",
"exec 1> >(logger -s -t $(basename $0)) 2>&1",
"yum -y install nginx; chkconfig nginx on; service nginx start"
]
]
}
}
}
}
我们使用DependsOn
确保 RDS 实例在autoScalingGroup
之前创建:
"autoScalingGroup" : {
"Type" : "AWS::AutoScaling::AutoScalingGroup",
"Properties" : {
"LaunchConfigurationName" : { "Ref" : "launchConfiguration" },
"DesiredCapacity" : "1",
"MinSize" : "1",
"MaxSize" : "1",
"LoadBalancerNames" : [ { "Ref" : "elbInstance" } ],
"VPCZoneIdentifier" : [ { "Ref" : "privateSubnet1" }, { "Ref" : "privateSubnet2" } ],
"Tags" : [ { "Key" : "Name", "Value" : "cloudformation-asg", "PropagateAtLaunch" : "true" } ]
},
"DependsOn" : "rdsInstance"
}
输出
同样,我们将使用这些输出,突出显示成功部署后某些资源属性。然而,Outputs
的另一个重要特点是,它们可以作为其他模板(堆栈)的输入参数。这在嵌套堆栈中非常有用。
注意
一旦声明,Outputs
就不能单独更新。你需要修改至少一个资源,才能触发Output
的更新。
我们将VPC ID
、NAT IP
地址和ELB DNS
名称作为Outputs
添加:
注意
"Outputs" : {
"vpcId" : {
"Description" : "VPC ID",
"Value" : { "Ref" : "vpc" }
},
"natEip" : {
"Description" : "NAT IP address",
"Value" : { "Ref" : "natEip" }
},
"elbDns" : {
"Description" : "ELB DNS",
"Value" : { "Fn::GetAtt" : [ "elbInstance", "DNSName" ] }
}
}
目前,一个模板最多可以有 60 个Outputs
。
操作
如果你一直在跟随教程,你现在应该在当前文件夹中有一个main.json
和一个parameters.json
。是时候开始使用它们了,接下来我们将进行一些操作:
-
验证模板
-
部署一个堆栈
-
更新堆栈
-
删除一个堆栈
模板验证
首先,我们使用validate-template
对 JSON 模板进行基本检查:
$ aws cloudformation validate-template --template-body file://main.json
{
"Description": "Provisions EC2, ELB, ASG and RDS resources",
"Parameters": [
{
"NoEcho": false,
"Description": "EC2 AMI ID",
"ParameterKey": "autoscalingGroupImageId"
}
如果没有错误,CLI 将返回解析后的模板。请注意,我们也可以使用--template-url
指向一个远程位置,而不是-template-body
。
部署堆栈
要部署我们的模板(堆栈),我们将使用create-stack
。它需要一个任意名称、模板的位置以及包含参数值的文件:
$ aws cloudformation create-stack --stack-name cfn-test --template-body
file://main.json --parameters file://parameters.json
{
"StackId": "arn:aws:cloudformation:us-east-1:xxxxxx:stack/cfn-test/xxxxxx"
}
CloudFormation 开始创建堆栈,此时没有进一步的输出返回。要获取 CLI 上的进度信息,请使用describe-stacks
:
$ aws cloudformation describe-stacks --stack-name cfn-test
{
"Stacks": [
{
"StackId": "arn:aws:cloudformation:us-east-xxxxxx:stack/cfn-test/xxxxxx"
...
"CreationTime": "2016-05-29T20:07:17.813Z",
"StackName": "cfn-test",
"NotificationARNs": [],
"StackStatus": "CREATE_IN_PROGRESS",
"DisableRollback": false
}
]
}
若想获取更多详细信息,请使用describe-stack-events
。
几分钟后(基于我们的小模板),StackStatus
从CREATE_IN_PROGRESS
变为CREATE_COMPLETE
,并返回我们请求的Outputs
:
$ aws cloudformation describe-stacks --stack-name cfn-test
"Outputs": [
{
"Description": "VPC ID",
"OutputKey": "vpcId",
"OutputValue": "vpc-xxxxxx"
},
{
"Description": "NAT IP address",
"OutputKey": "natEip",
"OutputValue": "x.x.x.x"
},
{
"Description": "ELB DNS",
"OutputKey": "elbDns",
"OutputValue": "cloudformation-elb-xxxxxx.us-east-1.elb.amazonaws.com"
}
],
"CreationTime": "2016-05-29T20:07:17.813Z",
"StackName": "cfn-test",
"NotificationARNs": [],
"StackStatus": "CREATE_COMPLETE",
"DisableRollback": false
此时,elbDNS
URL 应该返回 nginx 欢迎页面,如下所示:
如果没有返回,可能需要等待更多时间,直到 EC2 节点完全初始化。
更新堆栈
CloudFormation
提供了两种更新已部署堆栈的方法。
注意
一些更新操作可能是破坏性的(请参阅docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html
)。你应该始终检查你计划修改的资源的CloudFormation
文档,看看更改是否会导致任何中断。
如果你希望快速部署一个小的变更,你只需要修改模板文件并直接使用update-stack
进行部署:
$ aws cloudformation update-stack --stack-name cfn-test
--template-body file://main.json
--parameters file://parameters.json
否则,好的做法是使用Change Sets
预览堆栈的更改,然后再进行部署。例如,我们可以像之前那样更新 ELB 安全组中的规则:
-
修改
main.json
模板(向elbSecurityGroup
添加另一个规则):"elbSecurityGroup" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "SecurityGroupIngress" : [ { "ToPort" : "80", "FromPort" : "80", "IpProtocol" : "tcp", "CidrIp" : "0.0.0.0/0" }, { "ToPort" : "443", "FromPort" : "443", "IpProtocol" : "tcp", "CidrIp" : "0.0.0.0/0" } ]
-
创建变更集
:
$ aws cloudformation create-change-set --change-set-name updatingElbSecGroup --stack-name cfn-test --template-body file://main.json --parameters file://parameters.json
-
预览变更集:
$ aws cloudformation describe-change-set --change-set-name updatingSecGroup --stack-name cfn-test
-
执行变更集:
$ aws cloudformation execute-change-set --change-set-name updatingSecGroup --stack-name cfn-test
提示
无论是通过变更集还是直接更新,如果您仅仅是在修改参数值(
parameters.json
),则可以跳过重新上传模板(main.json
),使用--use-previous-template
。
删除堆栈
为了清理我们的实验,我们需要向 CloudFormation IAM 用户授予临时管理员权限(与之前 TF 部分相同的程序);运行delete-stack
:
$ aws cloudformation delete-stack --stack-name cfn-test
然后撤销管理员权限。
总结
在这一章中,我们讨论了基础设施即代码的重要性和实用性,以及如何使用Terraform
或 AWSCloudFormation
实现这一点。
我们检查了 TF 和 CF 模板的结构和各个组成部分,然后通过 CLI 实践将其部署到 AWS。我相信我们通过的示例展示了将基础设施作为代码进行部署的好处和立竿见影的收益。
然而,到目前为止,我们只完成了一半的工作。随着配置阶段的完成,您自然会希望开始配置您的基础设施,这就是我们将在下一章关于配置管理中做的事情。
第三章:将你的基础设施纳入配置管理
正如上一章结尾所暗示的那样,在我们能够宣称完全实现基础设施即代码(IaC)之前,还需要做一些工作。
第一步是将我们基础设施的硬件部分描述成代码;现在是时候关注它的软件或配置部分了。
假设我们已经配置了几个 EC2 节点,并希望在它们上安装某些软件包,并更新相关的配置文件。在配置管理(CM)工具流行之前,这类任务通常是由工程师手动执行的,工程师要么按照检查清单操作,要么运行一系列 Shell 脚本,或者两者兼而有之。正如你所想象的那样,这种方法扩展性较差,因为通常意味着一个工程师一次设置一台服务器。
此外,检查清单或脚本:
-
当涉及到配置主机以及在其上运行的完整应用程序栈时,编写脚本是非常困难的。
-
通常是针对特定主机或应用程序的,且不具备很好的可移植性。
-
越是远离最初编写它们的人,理解起来就越困难。
-
构建脚本通常只会在主机配置时执行一次,因此配置会从那时起开始漂移。
幸运的是,现在很少有人使用这些,因为配置管理已经成为一种常见做法。让我们来看一下它的一些好处:
-
CM 使我们能够一次性声明机器的期望状态,然后根据需要反复重现该状态。
-
强大的抽象层处理了环境、硬件和操作系统类型等具体细节,使我们能够编写可重用的 CM 代码。
-
声明的机器状态代码易于阅读、理解和协作。
-
CM 部署可以同时在数十台、数百台或数千台机器上执行。
在 DevOps 时代,有许多 CM 工具可以选择。你可能已经听说过 Puppet、Chef、Ansible、OpsWorks,或者我们将要使用的SaltStack(Salt Open 项目)。
这些都是成熟且复杂的 CM 解决方案,背后有活跃的社区支持。我很难证明有任何一个比其他的更好,因为它们都能很好地完成工作,各有自己的优缺点。所以,最终选择使用哪一个,通常取决于个人偏好。
无论你最终使用哪个工具,我想强调两点的重要性:命名规范和代码重用性。
在编写代码时遵循命名约定显然是一个优势,因为它能保证其他人理解你的工作时所需的努力更少。然而,除了编写代码,CM 还涉及到对节点执行代码,而这时命名变得尤为重要。想象一下你有四台服务器:leonardo、donatello、michelangelo 和 raphael。其中两台是前端层,两台是后端层,于是你坐下来分别为它们编写配置管理清单:webserver-node 和 database-node。到目前为止,一切都很好,考虑到主机数量,你可以启动你的 CM 工具,并轻松地告诉它在每台机器上运行相应的清单。
现在,想象一下 50 台,然后是 100 台主机,采用类似的扁平命名模式,你就会开始看到问题。当你的基础设施的规模和复杂性增加时,你将需要一种能够自然形成层次结构的主机命名约定。像 webserver-{0..10}、db-{0..5} 和 cache-{0..5} 这样的主机名可以进一步分组为前端和后端,然后以结构化、层次化的方式表示。基于角色或其他属性对节点进行分组的这种方式,在应用配置管理时极为有用。
在开始编写 CM 代码(清单)时,你应该已经开始考虑代码的重用性。你会发现,一般有两种方式来处理这个任务。你可以写一个大型的,例如,包含设置防火墙、一些 CLI 工具、NGINX 和 PHP 的 web 服务器部分,或者你可以将其拆分为更小的部分,比如 iptables、utils、NGINX、PHP 等等。
在我看来,后者的设计在编写清单时增加了一些开销,但重用性的好处是相当可观的。与其为每种服务器类型编写一大堆特定的声明,不如维护一组通用的小声明,并从中挑选出适合当前机器的部分。
举个例子:
manifests: everything_a_websrv_needs, everything_for_a_db, cache_main
nodes: web01, db01, cache01
CM_execution: web01=(everything_a_websrv_needs), db01=(everything_for_a_db), cache01=(cache_main)
或者更好的方法是:
manifests: iptables, utils, nginx, postgresql, redis, php
nodes: web01, db01, cache01
CM_execution: web01=(iptables,utils,nginx,php), db01=(iptables,utils,postgresql), cache01=(iptables,utils,redis)
SaltStack 入门
SaltStack(见 saltstack.com/
),首次发布于 2011 年,是一个自动化套件,提供配置管理以及标准的和/或事件驱动的编排。它通常在主从架构中使用,其中主节点提供跨计算资源的集中控制。得益于用于盐主与从节点之间通信的快速且轻量级的消息总线(ZeroMQ),它以其速度和可扩展性而著称。它也可以以无代理的方式使用,在这种方式下,从节点通过 SSH 被控制,这与 Ansible 的操作方式类似。
SaltStack 是用 Python 编写的,且易于扩展。你可以为其编写自己的模块,将长期运行的进程附加到它的事件总线上,并在不寻常的地方注入原始的 Python 代码。
主从模型非常强大,提供了很多灵活性,如果你需要管理的不仅仅是几个开发节点,并且想充分利用 SaltStack 的所有功能,这是推荐的方式。
注意
关于如何启动和运行 salt-master 的更多信息可以在这里找到:docs.saltstack.com/en/latest/topics/configuration/index.html
在我们的案例中,我们将探索使用 SaltStack 进行配置管理的强大功能,使用独立模式或无主模式。我们将复用上一章中的部分 Terraform 模板,启动一组 EC2 资源,启动一个 SaltStack minion,并让它自行配置,以便提供 Web 应用程序服务。
如果一切顺利,我们最终应该能够在负载均衡器(EC2 ELB)后面配置一个完全设置好的 Web 服务器(EC2 节点)。
这里是我们的任务列表:
-
准备我们的 SaltStack 开发环境。
-
编写我们希望 SaltStack 应用到节点的配置。
-
编写描述我们基础设施的 Terraform 模板。
-
通过 Terraform 部署基础设施,并让 SaltStack 配置它。
准备
SaltStack 配置管理通过以下主要组件进行:
-
States 是描述机器期望状态的文件。在这里,我们编写安装包、修改文件、更新权限等操作的指令。
-
Pillars 是我们定义变量的文件,帮助让 States 更加便捷和灵活。
-
Grains 是在 minion 主机上收集的信息。这些信息包括操作系统、环境、硬件平台等详细信息。
-
Salt 文件服务器 存储任何可能在 States 中引用的文件、脚本或其他工件。
-
Salt Top 文件用于将 States 和/或 Pillars 映射到 minions。
在主从模式下,除了 Grains 以外,所有这些组件都将由 salt-master 托管,并提供给 minions(其他后端也受到支持)。
我们计划以无主模式运行 Salt,这意味着我们需要一种方式将任何 States、Pillars 和相关文件从本地环境传输到 minion。Git?好主意。我们将在本地编写所有 Salt 代码,将其推送到 Git 仓库,然后在每个 minion 启动时将其检出。
关于选择 Git 托管解决方案,Github 或 Bitbucket 都是非常优秀的服务,但让我们的 minion EC2 节点访问这些服务需要一些密钥管理。相比之下,CodeCommit(AWS 的 Git 解决方案)通过 IAM 角色与 EC2 实例的集成更为顺畅。
让我们首先创建一个新的 IAM 用户和一个 CodeCommit Git 仓库。我们将使用用户的访问密钥来创建仓库,并使用 SSH 密钥克隆并与之工作:
-
在 AWS 控制台中,创建一个 IAM 用户(记下生成的访问密钥),并将AWSCodeCommitFullAccess内置/ 托管 IAM 策略附加到它,如下图所示:
-
在同一页面上,切换到安全凭证选项卡,并点击上传 SSH 公钥,如下面的截图所示:
-
配置
awscli
:$ export AWS_ACCESS_KEY_ID='AKIAHNPFB9EXAMPLEKEY' $ export AWS_SECRET_ACCESS_KEY= 'rLdrfHJvfJUHY/B7GRFTY/VYSRwezaEXAMPLEKEY' $ export AWS_DEFAULT_REGION='us-east-1'
-
创建一个仓库:
$ aws codecommit create-repository --repository-name salt --repository-description "SaltStack repo" { "repositoryMetadata": { "repositoryName": "salt", "cloneUrlSsh": "ssh://git-codecommit.us- east-1.amazonaws.com/v1/repos/salt", "lastModifiedDate": 1465728037.589, "repositoryDescription": "SaltStack repo", "cloneUrlHttp": "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/salt", "creationDate": 1465728037.589, "repositoryId": "d0628373-d9a8-44ab-942a-xxxxxx", "Arn": "arn:aws:codecommit:us-east-1:xxxxxx:salt", "accountId": "xxxxxx" } }
-
在本地克隆新仓库:
$ git clone ssh://SSH_KEY_ID@git-codecommit.us- east-1.amazonaws.com/v1/repos/salt Cloning into 'salt'... warning: You appear to have cloned an empty repository. Checking connectivity... done.
在这里,SSH_KEY_ID
是我们在步骤 2 上传公钥后看到的那个。
注意
有关连接到 CodeCommit 的更多选项,请参见docs.aws.amazon.com/codecommit/latest/userguide/setting-up.html
我们准备开始填充我们空的、新的 Salt 仓库。
编写配置管理代码
为了让 SaltStack 帮助我们将节点配置为 Web 服务器,我们需要告诉它这些应该是什么样的。在配置管理术语中,我们需要描述机器的期望状态。
在我们的示例中,我们将使用 SaltStack 状态、Pillars、Grains 和 Top 文件的组合来描述以下过程:
-
创建 Linux 用户帐户
-
安装服务(NGINX 和 PHP-FPM)
-
配置和运行已安装的服务
状态
一个状态包含一组我们希望应用于 EC2 从节点的指令。我们将在从节点上使用/srv/salt/states
作为 Salt 状态树的根目录。状态可以以单个文件的形式存储在其中,例如/srv/salt/states/mystate.sls
,或者组织成文件夹,如/srv/salt/states/mystate/init.sls
。稍后,当我们请求执行mystate
时,Salt 将查找状态树根目录中的state_name.sls
或state_name/init.sls
。我觉得第二种方式更整洁,因为它允许将其他与状态相关的文件保存在相关的文件夹中。
我们从管理 Linux 用户帐户的状态开始配置我们的 Web 服务器节点。在我们的 Salt Git 仓库中,我们创建states/users/init.sls
:
注意
veselin:
user.present:
- fullname: Veselin Kantsev
- uid: {{ salt'pillar.get' }}
- password: {{ salt'pillar.get' }}
- groups:
- wheel
ssh_auth.present:
- user: veselin
- source: salt://users/files/veselin.pub
- require:
- user: veselin
sudoers:
file.managed:
- name: /etc/sudoers.d/wheel
- contents: '%wheel ALL=(ALL) ALL'
我们将使用 YAML 编写大多数 Salt 配置。你会注意到在前一部分使用了三种不同的状态模块:
-
user.present
:此模块确保指定的用户帐户在系统上存在,或在必要时创建该帐户 -
ssh_auth.present
:用于管理用户 SSHauthorized_keys
文件的模块 -
file.managed
:用于创建/修改文件的模块
注意
SaltStack 的状态模块提供了丰富的功能。有关每个模块的详细信息,请参见docs.saltstack.com/en/latest/ref/states/all/
为了避免在user.present
下硬编码某些值,我们利用了 SaltStack 的 Pillars 系统。我们稍后会查看一个 pillar 文件,但现在只需注意在我们的状态中引用 pillar 值的语法。
这里还有另外两个要点值得关注:我们密钥文件的来源和require
属性。在这个例子中,salt://
格式的源地址指向 Salt 文件服务器,默认情况下从状态树中提供文件(有关支持的后端,请参见 docs.saltstack.com/en/latest/ref/file_server/
)。require
语句强制执行执行顺序,确保在尝试为其创建authorized_keys
文件之前,用户帐户已经存在。
注意
SaltStack 遵循一种命令式执行模型,直到强制执行自定义顺序时,才会调用声明式模式(见 docs.saltstack.com/en/latest/ref/states/ordering.html
)。
得益于 YAML 的可读性,人们可以轻松地看出这里发生了什么:
-
我们创建一个新的 Linux 用户。
-
我们应用所需的属性(uid、密码、组等)。
-
我们为其部署一个 SSH
authorized_keys
文件。 -
我们为用户所在的 wheel 组启用
sudo
。
或许你可以尝试编辑这个状态并为自己添加一个用户?这在我们部署之后会很有用。
我们现在将继续进行 NGINX 的安装,通过states/nginx/init.sls
。
注意
我们使用pkg.installed
模块安装 NGINX:
pkg.installed: []
设置服务在启动时启动(enable: True
),在可能的情况下启用重新加载而不是重新启动(reload: True
),确保在运行服务(service.running:
)之前安装了 NGINX 软件包(require:
)。
nginx:
service.running:
- enable: True
- reload: True
- require:
- pkg: nginx
然后放置一个config
文件(file.managed:
),确保服务等待这个文件的存在(require_in:
),并在每次文件更新时重新加载(watch_in:
):
/etc/nginx/conf.d/default.conf:
file.managed:
- source: salt://nginx/files/default.conf
- require:
- pkg: nginx
- require_in:
- service: nginx
- watch_in:
- service: nginx
请注意require
/require_in
和watch
/watch_in
的配对。每对中每个要求与其_in
对应物的区别在于它们作用的方向。
例如:
nginx:
service.running:
- watch:
- file: nginx_config
nginx_config:
file.managed:
- name: /etc/nginx/nginx.conf
- source: salt://...
与以下效果相同:
nginx:
service.running: []
nginx_config:
file.managed:
- name: /etc/nginx/nginx.conf
- source: salt://...
- watch_in:
- service: nginx
在这两种情况下,NGINX 服务会在config
文件更改时重新启动;然而,你可以看到第二种格式在你离开服务块(例如在不同的文件中)时可能会变得非常有用,正如我们将在下一个状态中看到的那样。
添加一些 PHP(states/php-fpm/init.sls
):
注意
include:
- nginx
php-fpm:
pkg.installed:
- name: php-fpm
- require:
- pkg: nginx
service.running:
- name: php-fpm
- enable: True
- reload: True
- require_in:
- service: nginx...
在这里,您可以更好地看到 _in
必要条件的有用性。当我们在顶部包含 nginx
state 后,require_in
会确保 nginx
在 php-fpm
启动之前不会启动。
配置好 NGINX 和 PHP-FPM 后,我们来添加一个简单的测试页面(states/phptest/init.sls
)。
注意
我们设置了一些从 Grains 中提取的变量(稍后会详细介绍):
{% set publqic_ipv4 = salt'cmd.shell' %}
{% set grains_ipv4 = salt'grains.get' %}
{% set grains_os = salt'grains.get' %}
{% set grains_osmajorrelease = salt'grains.get' %}
{% set grains_num_cpus = salt'grains.get' %}
{% set grains_cpu_model = salt'grains.get' %}
{% set grains_mem_total = salt'grains.get' %}
然后我们部署测试页面并直接向其中添加 contents
:
phptest:
file.managed:
- name: /var/www/html/index.php
- makedirs: True
- contents: |
<?php
echo '<p style="text-align:center;color:red">
Hello from {{ grains_ipv4 }}/{{ public_ipv4 }} running PHP ' .
phpversion() . ' on {{ grains_os }} {{ grains_osmajorrelease }}.
<br> I come with {{ grains_num_cpus }} x {{ grains_cpu_model }}
and {{ grains_mem_total }} MB of memory. </p>';
phpinfo(INFO_LICENSE);
?>
我们将在部署后使用此页面检查 NGINX 和 PHP-FPM 是否正常运行。
Pillars
现在让我们来看一下 Salt 存储变量的主要机制——Pillars。它们是:
-
类似树状的数据结构
-
在 salt-master 上定义/渲染,除非在无主模式下运行,此时它们存在于 minion 上
-
用于在中央位置存储变量,以便 minions 共享(除非它们是无主模式的)
-
有助于保持 States 的可移植性
-
适用于敏感数据(它们也可以进行 GPG 加密;请参见
docs.saltstack.com/en/latest/ref/renderers/all/salt.renderers.gpg.html
)
我们将在 minion 上使用 /srv/salt/pillars
作为 Pillar 树的根目录。让我们回到 users
state,查看以下几行:
- uid: {{ salt'pillar.get' }}
- password: {{ salt'pillar.get' }}
uid
和 password
属性设置为从名为 users
的 pillar 中获取。如果我们检查我们的 Pillar 树,将会找到一个 /srv/salt/pillars/users.sls
文件,其中包含:
users:
veselin:
uid: 5001
password: '$1$wZ0gQOOo$HEN/gDGS85dEZM7QZVlFz/'
现在可以轻松看到 users:veselin:password
在 state 文件中的引用是如何与该 pillar 结构匹配的。
注意
有关 pillar 使用的更多详细信息和示例,请参见:docs.saltstack.com/en/latest/topics/tutorials/pillar.html
Grains
与 Pillars 不同,Grains 被认为是静态数据:
-
它们在 minion 端生成,且不会在不同的 minions 之间共享
-
它们包含关于 minion 本身的事实
-
典型示例有 CPU、操作系统、网络接口、内存和内核
-
可以为一个 minion 添加自定义 Grains
我们已经在前面的测试页面(states/phptest/init.sls
)中很好地使用了 Grains,获取了包括 CPU、内存、网络和操作系统等各种主机详细信息。使用这些数据的另一种方式是在处理多操作系统环境时。我们来看下面的示例:
pkg.installed:
{% if grains['os'] == 'CentOS' or grains['os'] == 'RedHat' %}
- name: httpd...
{% elif grains['os'] == 'Debian' or grains['os'] == 'Ubuntu' %}
- name: apache2
...
{% endif %}
如您所见,Grains 与 Pillars 类似,帮助使我们的 States 更加灵活。
Top 文件
我们现在已经准备好了 States,甚至支持了一些 Pillars,并且理想情况下希望将所有这些应用到主机上,以便进行配置并准备好使用。
在 SaltStack 中,Top 文件提供了 States/Pillars 和它们应该应用的守卫之间的映射关系。我们在状态树和支柱树的根目录中都有一个 Top 文件(top.sls
)。我们碰巧只有一个环境(base),但我们可以轻松地添加更多(dev、qa、prod)。每个环境都可以有独立的状态和支柱树,以及独立的 Top 文件,这些文件会在运行时编译成一个文件。
注意
更多关于多环境设置的信息,请参见 docs.saltstack.com/en/latest/ref/states/top.html
。
让我们看看一个 top.sls
示例:
base:
'*':
- core_utils
- monitoring_client
- log_forwarder
'webserver-*':
- nginx
- php-fpm
'dbserver-*':
- pgsql_server
- pgbouncer
我们声明在我们的基本(默认)环境中:
-
所有守卫应该安装核心工具集、监控和日志转发代理。
-
匹配
webserver-*
的守卫,将获得nginx
和php-fpm
状态(除了之前的三个)。 -
数据库节点应用:三个常见的节点加上
pgsql_server
和pgbouncer
当你包含 Pillars、Grains 或这些的混合时,守卫的目标定位变得更加有趣(请参见 docs.saltstack.com/en/latest/ref/states/top.html#advanced-minion-targeting
)。
通过指定这样的状态/支柱与守卫的关联,从安全角度来看,我们也创建了一个有用的隔离。假设我们的 Pillars 包含敏感数据,那么我们可以通过这种方式限制可以访问这些数据的守卫群体。
回到我们的 Salt 仓库,我们发现有两个 top.sls
文件:
salt/states/top.sls
:
base:
'*':
- users
- nginx
- php-fpm
- phptest
salt/pillars/top.sls
:
base:
'*':
- users
我们可以允许自己使用 *
作为目标,因为我们在无主模式下运行,实际上我们所有的 States/Pillars 都是针对本地守卫的。
我们通过在守卫配置文件(/etc/salt/minion.d/masterless.conf
)中设置几个选项来启用此模式。
注意
这些有效地告诉盐守卫进程,Salt 文件服务器、状态树和支柱树都位于本地文件系统中。稍后你将看到如何通过 UserData 部署此配置文件。
注意
更多关于运行无主模式的信息,请访问:docs.saltstack.com/en/latest/topics/tutorials/standalone_minion.html
这结束了我们的 SaltStack 内部工作会议。当你更熟悉后,你可能想深入了解 Salt 引擎、Beacons、自定义模块和/或 Salt 配方。这些只是项目中不断添加的“忍者”功能的一部分。
在这一阶段,我们已经知道如何使用 Terraform 来部署,现在使用 SaltStack 来进行配置。
在配置管理下启动节点(端到端 IaC)
不再耽搁,让我们重新部署旧的 VPC,并在其中配置一个配置管理的 web 服务。
Terraform 将创建 VPC、ELB 和 EC2 节点,然后通过 EC2 UserData 引导 SaltStack 工作流。自然地,我们力求复用尽可能多的代码;然而,我们的下一次部署需要对 TF 模板进行一些修改。
注意
请参阅:github.com/PacktPublishing/Implementing-DevOps-on-AWS/tree/master/5585_03_CodeFiles/Terraform
。
resources.tf
:
-
这次我们不需要私有子网/路由表、NAT,也不需要 RDS 资源,因此我们已将这些内容移除,使得部署速度稍微加快了一些。
-
我们将使用 IAM 角色授予 EC2 节点访问 CodeCommit 仓库的权限。
-
我们已声明了角色:
resource "aws_iam_role" "terraform-role" { name = "terraform-role"path = "/"...
-
我们已为该角色添加并关联了一个策略(授予对 CodeCommit 的读取权限):
resource "aws_iam_role_policy" "terraform-policy" { name = "terraform-policy" role = "${aws_iam_role.terraform-role.id}"...
-
我们已为该角色创建并关联了实例配置文件:
resource "aws_iam_instance_profile" "terraform-profile" { name = "terraform-profile" roles = ["${aws_iam_role.terraform-role.name}"] ...
-
我们已更新 Auto Scaling 启动配置,加入了实例配置文件 ID:
resource "aws_launch_configuration" "terraform-lcfg" {...iam_instance_profile = "${aws_iam_instance_profile.terraform-profile.id}" ...
-
-
我们已更新了 UserData 脚本,加入了部分 SaltStack 引导指令,用于安装 Git 和 SaltStack,检出并部署我们的 Salt 代码,最后运行 Salt:
user_data = <<EOF #!/bin/bash set -euf -o pipefail exec 1> >(logger -s -t $(basename $0)) 2>&1 # Install Git and set CodeComit connection settings # (required for access via IAM roles) yum -y install git git config --system credential.helper '!aws codecommit credential-helper $@' git config --system credential.UseHttpPath true # Clone the Salt repository git clone https://git-codecommit.us-east-1.amazonaws.com/v1/repos/ salt/srv/salt; chmod 700 /srv/salt # Install SaltStack yum -y install https://repo.saltstack.com/yum/amazon/ salt-amzn-repo-latest-1.ami.noarch.rpm yum clean expire-cache; yum -y install salt-minion; chkconfig salt-minion off # Put custom minion config in place (for enabling masterless mode) cp -r /srv/salt/minion.d /etc/salt/ # Trigger a full Salt run salt-call state.apply EOF We have moved our EC2 node (the Auto Scaling group) to a public subnet and allowed incoming SSH traffic so that we can connect and play with Salt on it: resource "aws_security_group" "terraform-ec2" {ingress { from_port = "22" to_port = "22" ...resource "aws_autoscaling_group" "terraform-asg" { ... vpc_zone_identifier = ["${aws_subnet.public-1.id}", ...
variables.tf
:
我们已移除所有与 RDS 相关的变量。
outputs.tf
:
我们已移除与 RDS 和 NAT 相关的输出。
iam_user_policy.json
:
这份文档很快会派上用场,因为我们需要为部署创建一个新用户。我们已移除 RDS 权限,并从中添加了 IAM 权限。
我们现在准备好进行部署了。飞行前检查:
-
更新了 Terraform 模板
注意
请参阅:
github.com/PacktPublishing/Implementing-DevOps-on-AWS/tree/master/5585_03_CodeFiles/Terraform
)这些文件已在我们指定的 terraform 文件夹中本地保存。 -
根据
iam_user_policy.json
中的新权限集,创建/更新了我们的 Terraform IAM 账户。 -
确保我们拥有
terraform ec2 keypair
的副本(以后用于 SSH 登录) -
我们的所有 SaltStack 代码已推送到 Salt CodeCommit 仓库中(请参阅:
git-codecommit.us-east-1.amazonaws.com/v1/repos/salt
)
让我们导出凭据并启动 Terraform:
$ export AWS_ACCESS_KEY_ID='user_access_key'
$ export AWS_SECRET_ACCESS_KEY='user_secret_access_key'
$ export AWS_DEFAULT_REGION='us-east-1'$ cd Terraform/$ terraform validate
$ terraform plan...Plan: 15 to add, 0 to change, 0 to destroy.
$ terraform apply...Outputs:
ELB URI = terraform-elb-xxxxxx.us-east-1.elb.amazonaws.com
VPC ID = vpc-xxxxxx
请等待 3-5 分钟,直到输出的 t2.nano
实例就绪,然后从以下输出的 ELB URI 进行访问:
胜利!
尝试在 terraform.tfvars
中增加 autoscaling-group-minsize 和 autoscaling-group-maxsize 的值,然后重新应用模板。刷新页面后,你应该能看到不同的 IP 地址。
根据前面的测试页面,我们可以合理地相信 Salt 已经成功地引导并应用了我们的 States 配置。
然而,我们确实启用了 SSH 访问,以便能更进一步地实验 Salt,所以让我们开始吧。
我们在测试页面上看到了节点的公共 IP。您可以使用terraform ec2 keypair
或者默认的ec2-user
Linux 账户通过 SSH 登录,或者如果您之前在users/init.sls
状态中为自己创建了一个账户,现在也可以使用它。
一旦连接,我们可以使用salt-call
命令(以 root 身份)与 Salt 进行本地交互:
- 那么,Pillars 呢:
# salt-call pillar.items
- 或者让我们看看有什么 Grains 可用:
# salt-call grains.items
- 运行单独的 States:
# salt-call state.apply nginx
- 或执行完整的运行,即根据 Top 文件执行所有分配的 States:
# salt-call state.apply
在尝试了一段时间的新部署后,我猜您会想尝试添加或更改 States/Pillars 或 Salt 代码的其他部分。根据我们之前商定的 IaC 规则,我们做的每个更改都需要通过 Git,但让我们检查一下之后部署这些更改的选项:
-
将更改拉取到每个 minion 并运行
salt-call
-
配置新的 minions,拉取最新的代码
-
通过 Salt-master 推送更改
很容易看出,第一种选项在我们用于测试的几个节点上会有效,但在大规模使用时很快就会变得难以管理。
每次部署时配置新的 minions 是一个有效的选项,如果更倾向于使用无主 Salt 设置;然而,您需要考虑您环境中部署的频率以及替换 EC2 节点的相关成本。这里值得一提的一个好处是蓝绿部署。通过配置新的 minions 来服务您的代码更改,您可以保留旧的 minions 一段时间,这样可以逐步切换流量,并在需要时安全地回滚。
拥有一个 Salt-master 是我推荐的任何非开发环境的方式。Salt 代码保存在其中,因此您所做的任何 Git 更改,只需要拉取一次。然后,您可以通过 Salt-master 部署已更改的 States/Pillars 到您想要的 minions。您仍然可以对重大版本使用蓝绿部署,或者如果只是一个小的安全修复或可能需要立即传递给所有 minions 的关键更改,您也可以直接部署到当前的 minions。
Salt-master 的另一个强大功能是协调,具体来说是远程执行。通过将所有的 minion 连接到它,salt-master 成为一个指挥中心,从中你可以或多或少地完全控制它们。
在 minions 上执行命令是通过模块完成的,从通用的cmd.run
模块(本质上允许您运行任意的 shell 命令)到更专业的模块,如nginx
、postfix
、selinux
或zfs
。正如您在这里看到的,模块列表相当长:docs.saltstack.com/en/latest/ref/modules/all/index.html
。
如果你记得之前关于主机名和命名约定的部分,你会更能体会到它们的价值。在此,能够执行如下命令非常方便:
salt 'webserver-*' nginx.status
salt 'db-*' postgres.db_list
你还可以使用 Pillars 和/或 Grains 给你的主机添加标签,以便你可以根据位置、角色、部门或类似的条件进一步将它们分组。
简而言之,以下是 masterless 和 salt-master 布局的一些关键点:
Salt Master | Masterless |
---|
|
-
一个强大的集中式控制平台(必须得到充分安全保护),允许快速并行地访问庞大的 minions 网络
-
高级功能,如 Salt 引擎、Runners、Beacons、反应器系统
-
API 访问
|
-
无需维护 salt-master 节点
-
没有一个节点提供对其他所有节点的完全访问权限,从某种意义上来说,这更安全
-
更简便的 Salt 操作
-
在初始的 Salt 执行后,minions 可以被视为不可变的
|
对于许多 FOR LOOP 大师 来说,像 Salt 这样的并行执行工具非常有吸引力。它允许你以大规模迅速访问节点,无论你是想查询它们的运行时间、重新加载服务,还是在发生威胁警报时通过停止集群中的 sshd 来做出反应。
注意
在你离开之前,请记得删除前面示例中使用的任何 AWS 资源(VPC、ELB、EC2、IAM、CodeCommit 等),以避免产生意外费用。
总结
在本章中,我们研究了 基础设施即代码 的第二部分,即 配置管理。
我们了解了 CM 解决方案 SaltStack 的几个不同组件:States、Pillars、Grains 和 Top File。我们学习了如何使用它们以及如何为它们编写代码。
然后,我们将之前使用 Terraform 部署基础设施的知识与使用 SaltStack 配置基础设施的知识结合起来,最终完成了我们的第一次端到端 IaC 部署。
接下来,我们将深入探讨 持续集成:它是什么,如何在 AWS 上设置 CI 管道。
第四章。通过持续集成更快地构建、测试和发布
本章的重点将是快速迭代的价值:根据博伊德定律,快速优于高质量迭代(你可能记得在第一章提到的 OODA 原则,什么是 DevOps,以及你是否应该关心?)。
迭代是指软件开发周期,从代码编写、发布(提交到版本控制)、编译(如果需要)、测试,最后部署的过程。
持续集成(CI)定义了开发人员应该采用的常规和必要的工具,以使迭代尽可能快速。
让我们从人因开始:
-
使用版本控制(例如 Git)
-
提交较小的更改,更频繁
-
首先在本地测试
-
进行同伴代码审查
-
在问题解决之前暂停其他团队活动
然后添加一些自动化(一个 CI 服务器):
-
监控版本控制的变化(例如 Git 提交)
-
拉取已更改的代码
-
编译并运行测试
-
成功时,构建工件
-
失败时,通知团队并暂停管道
-
-
重复
提交较小的更改有助于更早发现问题,并更容易解决它们;开发者更频繁地收到关于他们工作的反馈,这增加了他们对自己代码状态的信心。
在本地进行测试,尽可能地这样做,能大大减少由于 CI 管道在小问题上卡壳而带来的团队分心。
代码审查在许多方面都是有益的。它们消除不良的编码习惯,因为同伴会确保代码符合约定的标准。它们增加了可见性;同伴能够更多地接触到他人的工作。它们帮助发现机器可能错过的错误。
丰田方式教我们在发现问题时要停线。在 CI 方面,这意味着在错误发生时暂停管道,集中资源修复这些错误。刚开始时,这看起来可能像是减少生产力并放慢整个过程的明显方式,但一次又一次地证明,最初的开销最终是值得的。通过这种方式,你可以将技术债务保持在最低限度;在开发过程中不断改进代码,防止问题积累并在后期重新浮现。现在是时候重申之前提到的本地测试的观点了。你可能不想因为一些微不足道的错误打扰同事,这些错误本可以在提交之前轻松发现。
当你成功建立了这个团队纪律(最困难的部分)之后,是时候通过设置 CI 管道加入一些自动化的味道了。
CI 服务器不知疲倦地监控你的代码仓库,并通过执行一组任务对变化作出反应。我相信显而易见的是,这为工程师节省了大量时间和精力,更不用说他们避免了处理这种单调工作的麻烦。
一个管道,比如在 Jenkins 中,通常由多个阶段组成:每个阶段代表拉取最新代码、在其上运行构建任务、执行测试然后构建产物,且每个阶段的运行都依赖于前一个阶段成功完成。
这通常描述了工程师的习惯与一些工具如何大大改善软件开发周期。持续集成帮助我们更好地协作、编写更好的代码、更频繁地发布,并更快地获取反馈。
用户希望快速发布新功能,开发人员希望看到自己工作的成果——每个人都能从中受益。
我们已经讨论了理论,现在让我们将注意力集中在本章的标题上。我们将使用之前学到的 Terraform 和 Salt 技能,在 AWS 上部署一个包含 Jenkins(v2)CI 服务器的 CI 环境。
Jenkins(参考:jenkins.io
)是一个流行且成熟的开源项目,专注于自动化。它有许多集成,支持各种平台和编程语言。认识 Jenkins:wiki.jenkins-ci.org/display/JENKINS/Meet+Jenkins
。
我们的 CI 环境部署可以分为三个主要阶段:
-
准备一个基础设施即代码部署:
-
编写Terraform模板以提供 VPC 和 EC2 实例
-
编写Salt状态以在 EC2 实例上安装 Jenkins、NGINX 和其他软件
-
-
部署 IaC:
- 部署 Terraform 模板和 Salt 状态
-
设置 CI:
- 配置 Jenkins 管道以实现演示应用程序的持续集成
准备 IaC
根据我们的基础设施即代码(IaC)原则,这次部署也将主要通过模板来完成。我们将尝试复用上一章的一些 Terraform 和 Salt 代码。
Terraform 模板
对于这个特定的设置,我们可以简化我们的模板,因为我们只需要 VPC、一些网络配置以及一个 EC2 实例。
让我们浏览一下我们TF仓库中的文件:
变量
我们需要的几个变量可以分为与 VPC 和 EC2 相关的:
注意
VPC
variable "aws-region" {
type = "string"
description = "AWS region"
}
variable "vpc-cidr" {
type = "string"
description = "VPC CIDR"
}
variable "vpc-name" {
type = "string"
description = "VPC name"
}
variable "aws-availability-zones" {
type = "string"
description = "AWS zones"
}
EC2
variable "jenkins-ami-id" {
type="string"
description = "EC2 AMI identifier"
}
variable "jenkins-instance-type" {
type = "string"
description = "EC2 instance type"
}
variable "jenkins-key-name" {
type = "string"
description = "EC2 ssh key name"
}
变量(值)
根据基本的变量定义,我们现在提供一些值:
注意
VPC
我们将把部署保持在美国东部(US East):
aws-region = "us-east-1"
vpc-cidr = "10.0.0.0/16"
vpc-name = "Terraform"
aws-availability-zones = "us-east-1b,us-east-1c"
EC2
一个 Nano 实例足以进行测试。请确保引用的密钥对存在:
jenkins-ami-id = "ami-6869aa05"
jenkins-instance-type = "t2.nano"
jenkins-key-name = "terraform"
资源
注意
创建 VPC
作为标准(良好)实践,我们将所有资源都创建在 VPC 内:
# Set a Provider
provider "aws" {
region = "${var.aws-region}"
}
# Create a VPC
resource "aws_vpc" "terraform-vpc" {
cidr_block = "${var.vpc-cidr}"
tags {
Name = "${var.vpc-name}"
}
}
添加网络组件
我们添加了一个网关、一张路由表,以及一个互联网连接子网,Jenkins 实例将从这里启动:
IGW
# Create an Internet Gateway
resource "aws_internet_gateway" "terraform-igw" {
vpc_id = "${aws_vpc.terraform-vpc.id}"
}
路由表
# Create public route tables
resource "aws_route_table" "public" {
vpc_id = "${aws_vpc.terraform-vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.terraform-igw.id}"
}
tags {
Name = "Public"
}
}
子网
# Create and associate public subnets with a route table
resource "aws_subnet" "public-1" {
vpc_id = "${aws_vpc.terraform-vpc.id}"
cidr_block = "${cidrsubnet(var.vpc-cidr, 8, 1)}"
availability_zone = "${element(split(",",var.aws-availability-zones), count.index)}"
map_public_ip_on_launch = true
tags {
Name = "Public"
}
}
resource "aws_route_table_association" "public-1" {
subnet_id = "${aws_subnet.public-1.id}"
route_table_id = "${aws_route_table.public.id}"
}
添加 EC2 节点及相关资源
我们的 Jenkins 节点的安全组需要允许 HTTP/S 访问以及 SSH 访问,方便我们在需要时访问命令行:
安全组
resource "aws_security_group" "jenkins" {
name = "jenkins"
description = "ec2 instance security group"
vpc_id = "${aws_vpc.terraform-vpc.id}"
ingress {
from_port = "22"
to_port = "22"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = "80"
to_port = "80"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = "443"
to_port = "443"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
IAM 角色
我们将使用 IAM 角色来授予 Jenkins 访问 AWS 服务的权限:
resource "aws_iam_role" "jenkins" {
name = "jenkins"
path = "/"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
IAM 角色策略
这个策略将允许 Jenkins 从 CodeCommit 仓库读取,并在 S3 存储桶上执行所有操作(除了删除):
resource "aws_iam_role_policy" "jenkins" {
name = "jenkins"
role = "${aws_iam_role.jenkins.id}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codecommit:Get*",
"codecommit:GitPull",
"codecommit:List*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"NotAction": [
"s3:DeleteBucket"
],
"Resource": "*"
}
]
}
EOF
}
IAM 配置文件
resource "aws_iam_instance_profile" "jenkins" {
name = "jenkins"
roles = ["${aws_iam_role.jenkins.name}"]
}
EC2 实例
在这里,我们定义了一个单实例,并且其引导 UserData 脚本:
resource "aws_instance" "jenkins" {
ami = "${var.jenkins-ami-id}"
instance_type = "${var.jenkins-instance-type}"
key_name = "${var.jenkins-key-name}"
vpc_security_group_ids = ["${aws_security_group.jenkins.id}"]
iam_instance_profile = "${aws_iam_instance_profile.jenkins.id}"
subnet_id = "${aws_subnet.public-1.id}"
tags { Name = "jenkins" }
在这里,我们设置了启动 EC2 实例所需的属性,比如实例类型、使用的 AMI、安全组、子网等等。
接下来,我们添加了引导 shell 脚本,帮助我们安装所需的软件包、检出 Git 仓库并运行 Salt:
user_data = <<EOF
#!/bin/bash
set -euf -o pipefail
exec 1> >(logger -s -t $(basename $0)) 2>&1
# Install Git and set CodeComit connection settings
# (required for access via IAM roles)
yum -y install git
git config --system credential.helper '!aws codecommit credential-helper $@'
git config --system credential.UseHttpPath true
# Clone the Salt repository
git clone https://git-codecommit.us-east-1.amazonaws.com/v1/repos/salt /srv/salt; chmod 700 /srv/salt
# Install SaltStack
yum -y install https://repo.saltstack.com/yum/amazon/salt-amzn-repo-latest-1.ami.noarch.rpm
yum clean expire-cache; yum -y install salt-minion; chkconfig salt-minion off
# Put custom minion config in place (for enabling masterless mode)
cp -r /srv/salt/minion.d /etc/salt/
# Trigger a full Salt run
salt-call state.apply
EOF
lifecycle { create_before_destroy = true }
}
弹性 IP
最后,我们为 Jenkins 配置一个静态 IP:
resource "aws_eip" "jenkins" {
instance = "${aws_instance.jenkins.id}"
vpc = true
}
输出
注意
一些有用的输出,用来提供 Jenkins 节点的地址:
output "VPC ID" {
value = "${aws_vpc.terraform-vpc.id}"
}
output "JENKINS EIP" {
value = "${aws_eip.jenkins.public_ip}"
}
这就是我们定义的 VPC 基础设施。现在我们可以开始配置 Salt 和应用栈了。
SaltStack 代码
你应该还记得上一章我们提到的那个最喜爱的配置管理工具。我们将使用 SaltStack 来为我们配置 EC2 Jenkins 节点。
状态
注意
top.sls
我们正在使用一个单一的从属节点(minion),所有的状态都应用于它:
base:
'*':
- users
- yum-s3
- jenkins
- nginx
- docker
users
我们添加了一个 Linux 用户账户,配置了其 SSH 密钥和 sudo 权限:
veselin:
user.present:
- fullname: Veselin Kantsev
- uid: {{ salt'pillar.get' }}
...
yum-s3
作为 CI 流水线的一部分,我们将在 S3 中存储 RPM 制品。Cob(参考:github.com/henrysher/cob
)是一个 Yum 包管理器插件,它使得可以通过 IAM 角色访问基于 S3 的 RPM 仓库。
我们部署插件、其配置和一个仓库定义(目前禁用)作为管理文件:
yum-s3_cob.py:
file.managed:
- name: /usr/lib/yum-plugins/cob.py
- source: salt://yum-s3/files/cob.py
yum-s3_cob.conf:
file.managed:
- name: /etc/yum/pluginconf.d/cob.conf
- source: salt://yum-s3/files/cob.conf
yum-s3_s3.repo:
file.managed:
- name: /etc/yum.repos.d/s3.repo
- source: salt://yum-s3/files/s3.repo
Jenkins
这里是主角——Jenkins 先生。我们在 CI 流水线中使用 Docker,因此接下来是 include
。Docker 使我们能够将不同的流水线步骤隔离开来,这让依赖管理变得更加容易,并有助于保持 Jenkins 节点的干净。
include:
- docker
同时我们确保安装 Java 和其他一些前提条件:
jenkins_prereq:
pkg.installed:
- pkgs:
- java-1.7.0-openjdk
- gcc
- make
- createrepo
然后,安装 Jenkins 本身:
jenkins:
pkg.installed:
- sources:
- jenkins: http://mirrors.jenkins-ci.org/redhat-stable/jenkins-2.7.1-1.1.noarch.rpm
- require:
- pkg: jenkins_prereq
...
NGINX
我们将使用 NGINX 作为反向代理和 SSL 终止点。这并不是说 Jenkins 不能单独提供服务,只是将角色分开被认为是更好的实践:
include:
- jenkins
nginx:
pkg.installed: []
...
{% for FIL in ['crt','key'] %}
/etc/nginx/ssl/server.{{ FIL }}:
...
{% endfor %}
Docker
是时候提到 Docker 了,鉴于它如今的(应得的)流行。它非常适合我们的 CI 需求,为可能需要的各种测试和构建提供隔离的环境:
docker:
pkg.installed: []
service.running:
- enable: True
- reload: True
Pillars
注意
top.sls
我们的独立 minion 获取了所有内容:
base:
'*':
- users
- nginx
users
设置 Linux 账户的密码哈希和一致的 UID:
users:
veselin:
uid: 5001
password: ...
NGINX
我们将 SSL 数据存储在此 Pillar 中:
nginx:
crt: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
key: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
Minion 配置
注意
masterless.conf
我们仍然在使用 Salt 的独立(无主)模式,因此这是我们额外的minion
配置:
file_client: local
file_roots:
base:
- /srv/salt/states
pillar_roots:
base:
- /srv/salt/pillars
由于前面的所有代码,我们应该能够运行 Terraform 并最终得到一个准备好使用的 Jenkins 服务。
让我们试试看。
部署基础设施即代码(IaC)
我们首先创建一个 Terraform EC2 密钥对和一个 Terraform IAM 用户,方法与前几章相同(不要忘记写下访问/密钥 API)。然后,我们授予 IAM 用户对 EC2、IAM、S3 和 CodeCommit 服务的操作权限:
注意
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"NotAction": [
"codecommit:DeleteRepository"
],
"Resource": "*"
},
{
"Effect": "Allow",
"NotAction": [
"s3:DeleteBucket"
],
"Resource": "*"
},
{
"Sid": "Stmt1461764665000",
"Effect": "Allow",
"Action": [
"ec2:AllocateAddress",
...
然后我们将一个 SSH 公钥与用户关联(按照上一章的截图)以允许 codecommit
仓库访问。
接下来,我们需要使用之前生成的密钥设置我们的 AWS CLI 环境:
$ export AWS_ACCESS_KEY_ID='user_access_key'
$ export AWS_SECRET_ACCESS_KEY='user_secret_access_key'
$ export AWS_DEFAULT_REGION='us-east-1'
现在我们应该能够使用 CLI 工具并创建我们的 SaltStack 仓库:
$ aws codecommit create-repository --repository-name salt
--repository-description "SaltStack repo"
{
"repositoryMetadata": {
"repositoryName": "salt",
"cloneUrlSsh":
"ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/salt",
...
我们在本地克隆仓库:
$ git clone ssh://SSH_KEY_ID@git-codecommit.us-east-
1.amazonaws.com/v1/repos/salt
Cloning into 'salt'...
warning: You appear to have cloned an empty repository.
Checking connectivity... done.
(其中SSH_KEY_ID
是我们在这里上传公钥后看到的)
最后,您可以复制本章的现成 Salt 代码示例,提交并推送到 codecommit
仓库。
注意
请参考:github.com/PacktPublishing/Implementing-DevOps-on-AWS/tree/master/5585_04_CodeFiles/CodeCommit/salt
在 SaltStack 仓库同步后,我们可以继续进行 Terraform 和引导过程。在我们的 TF 模板文件夹中,我们运行熟悉的命令序列:
$ terraform validate
$ terraform plan
Refreshing Terraform state prior to plan...
...
Plan: 11 to add, 0 to change, 0 to destroy.
$ terraform apply
aws_iam_role.jenkins: Creating...
...
Apply complete! Resources: 11 added, 0 changed, 0 destroyed.
Outputs:
JENKINS EIP = x.x.x.x
VPC ID = vpc-xxxxxx
最后,我们得到 Jenkins 节点的 IP 地址,之后需要将其解析为主机名(例如,通过 nslookup
命令)。在浏览器中加载该地址,您应该能看到 Jenkins 界面。
设置 CI
成功部署 Terraform 后,我们进入服务配置阶段。更具体地说,是 Jenkins 和集成流水线的配置。
Jenkins 初始化
在 Jenkins 第一次运行时,我们需要完成一个简短的设置过程。首先,我们需要 SSH 登录到节点,并获取存储在 /var/lib/jenkins/secrets/initialAdminPassword
中的管理员密码:
我们主要关心的是与建议插件一起包含的流水线插件组:
插件安装完成后,是时候创建第一个用户了:
这样,初始化过程完成,Jenkins 已经准备好使用:
编写一个示例应用
在配置 CI 流水线之前,最好先有些集成工作可做。一个简单的 Hello World 类型的 PHP 代码就足够了,所以我诚恳地向所有 PHP 开发者道歉,向您展示我们的示例应用源代码:
注意
src/index.php:
<?php
function greet($name) {
return "Hello $name!";
}
$full_name = "Bobby D";
greet ($full_name);
Clapping fades...
And naturally, a unit test for it:
tests/indexTest.php:
<?php
require_once "src/index.php";
class IndexTest extends PHPUnit_Framework_TestCase
{
public function testGreet() {
global $full_name;
$expected = "Hello $full_name!";
$actual = greet($full_name);
$this->assertEquals($expected, $actual);
}
}
在我们的 demo-app
文件夹中有一个第三个文件,名字很有意思,叫做 Jenkinsfile
,我们稍后会讨论它。
现在让我们将代码放入代码仓库:
$ aws codecommit create-repository --repository-name demo-app
--repository-description "Demo app"
{
"repositoryMetadata": {
"repositoryName": "demo-app",
"cloneUrlSsh":
"ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/demo-app"
...
Then we clone it locally (replace SSH_KEY_ID as before):
$ git clone ssh://SSH_KEY_ID@git-codecommit.us-east-
1.amazonaws.com/v1/repos/demo-app
...
最后,我们将 demo-app
代码放入空的仓库,提交并推送所有更改到 codecommit。
定义流水线
是时候决定 CI 流水线要为我们做些什么了。这里是一些有用步骤的列表,作为起点:
-
从 Git 中检出应用程序源代码
-
通过在 Docker 容器内运行 PHPUnit(在 Jenkins 主机上)来进行测试
-
通过在 Jenkins 主机上的容器中执行 FPM 来构建应用程序工件
-
将工件上传到外部存储(例如,Yum 仓库)
转换为 Jenkins 流水线代码:
注意
#!groovy
node {
stage "Checkout Git repo"
checkout scm
stage "Run tests"
sh "docker run -v \$(pwd):/app --rm phpunit/phpunit tests/"
stage "Build RPM"
sh "[ -d ./rpm ] || mkdir ./rpm"
sh "docker run -v \$(pwd)/src:/data/demo-app -v \$(pwd)/rpm:/data/rpm --rm tenzer/fpm fpm -s dir -t rpm -n demo-app -v \$(git rev-parse --short HEAD) --description "Demo PHP app" --directories /var/www/demo-app --package /data/rpm/demo-app-\$(git rev-parse --short HEAD).rpm /data/demo-app=/var/www/"
stage "Update YUM repo"
sh "[ -d ~/repo/rpm/demo-app/ ] || mkdir -p ~/repo/rpm/demo-app/"
sh "mv ./rpm/*.rpm ~/repo/rpm/demo-app/"
sh "createrepo ~/repo/"
sh "aws s3 sync ~/repo s3://MY_BUCKET_NAME/ --region us-east-1 --delete"
stage "Check YUM repo"
sh "yum clean all"
sh "yum info demo-app-\$(git rev-parse --short HEAD)"
}
一般来说,定义一个流水线包含一系列任务/阶段的设置。让我们回顾一下之前的每个阶段:
-
我们从 Git 检出
demo-app
代码开始。假设代码库地址与Jenkinsfile
的地址相同。 -
在下一个阶段,我们利用 Docker 的隔离性,启动一个容器,容器中包含运行 PHPUnit(参考:https://phpunit.de)所需的一切,以对我们的
demo-app
源代码进行测试。如果你想添加更多测试或进一步修改,可以查看${GIT_URL}/Examples/Chapter-4/CodeCommit/demo-app/
下的tests/
文件夹。 -
如果测试通过,我们就开始使用一个干净且用户友好的工具叫做 FPM(参考:https://github.com/jordansissel/fpm)在 Docker 容器中构建一个 RPM 工件。我们使用短的
git commit hash
作为我们的 demo-app 的版本标识。 -
我们将 RPM 工件移动到指定的仓库文件夹,使用
createrepo
将其创建为 YUM 仓库,并将所有数据同步到一个 Amazon S3 存储桶。目的是稍后使用这个基于 S3 的 YUM 仓库来部署我们的demo-app
。 -
最后,作为一个额外的步骤,我们检查一下我们刚刚同步的包是否可以通过 YUM 获取。
我们的流水线现在已定义,但在运行之前,我们需要满足一个(S3)依赖项。我们需要创建一个 S3 存储桶来存储流水线生成的 RPM 工件。然后,我们需要更新 Jenkins 和 Saltstack 代码中的部分内容,添加该 S3 存储桶的地址。
为了与 S3 进行交互,我们将在之前为 Terraform 配置的环境中使用 AWS CLI 工具:
$ aws s3 mb s3://MY_BUCKET_NAME
存储桶名称由你决定,但请记住,全球 S3 命名空间是共享的,所以名称越唯一越好。
接下来,我们更新我们的流水线定义(Jenkinsfile
)。查找包含MY_BUCKET_NAME
的行:
sh "aws s3 sync ~/repo s3://MY_BUCKET_NAME/ --region us-east-1
--delete"
我们还需要更新 SaltStack(再次替换MY_BUCKET_NAME
):
注意
[s3-repo]
name=S3-repo
baseurl=https://s3.amazonaws.com/MY_BUCKET_NAME
enabled=1
gpgcheck=0
这个repo
文件将在流水线的最后阶段使用,正如我们稍后将看到的那样。此时,你需要提交并推送这两项更改:将Jenkinsfile
提交到demo-app
仓库,将s3.repo
文件提交到 SaltStack 仓库。然后,你需要通过 SSH 连接到 Jenkins 节点,拉取并应用 Salt 更改。
设置流水线
回到 Jenkins 界面。在登录后,我们点击欢迎页面上的创建新作业
链接:
我们选择Pipeline作为作业类型,并为其选定一个名称:
下一屏幕将带我们进入作业配置的详细信息。顶部我们选择丢弃旧构建,以保持 Jenkins 工作空间的简洁。我们设定,只保留此作业最近五次执行的详细信息:
在构建触发器下,我们选择每 5 分钟轮询一次 Git 仓库以检查更改:
在下方,我们选择从 SCM 加载管道脚本,将 SCM 设置为Git,并添加我们demo-app
仓库的 URL(即git-codecommit.us-east-1.amazonaws.com/v1/repos/demo-app
)以进行轮询:
无需额外的凭证,因为这些凭证将通过 EC2 IAM 角色自动获取。请注意脚本路径,它引用了我们之前提到的 Jenkins 文件。这是一个很棒的新功能,它为我们提供了代码作为管道的功能,详细说明请见这里:jenkins.io/doc/pipeline/#loading-pipeline-scripts-from-scm
。
这样我们就可以方便地将应用程序代码和 Jenkins 管道定义一起放在版本控制中。
保存管道作业后,Jenkins 将开始轮询 Git 仓库,并在检测到更改时触发执行(或者你可以点击立即构建强制执行一次)。
每次成功构建后,都会将一个 RPM 包上传到我们的 YUM 仓库。继续进行实验,通过修改demo-app
源代码来破坏构建,使得测试失败。
要进行故障排除,请查看构建历史列表,选择失败的作业,并检查其控制台输出:
现在你已经熟悉了我们的示例管道,我鼓励你扩展它:添加更多的阶段,让一些任务并行执行,启用聊天或电子邮件通知,或者链接管道让它们触发彼此。
随着你将更多日常手动操作转化为 Jenkins 作业,你将会体会到实施 CI 服务器的好处。
你可以放心,你的团队成员也会喜欢这个。
注意
请记得删除之前示例中使用的任何 AWS 资源(如 VPC、EC2、S3、IAM、CodeCommit 等),以避免不必要的费用。
总结
本章中我们研究了如何在 AWS 上启动和配置持续集成环境的示例。
我们利用之前的 Terraform 和 SaltStack 知识来准备 AWS 基础设施。
在 Jenkins CI 的帮助下,我们构建了一个管道,它会获取应用程序源代码,对其进行测试,构建 RPM 包,并将其存储到远程 YUM 仓库中以备后用。
我们的下一个主题将是持续交付,这是持续集成的扩展,它让我们更接近能够有信心地将应用程序部署到生产环境。
第五章:通过持续交付随时准备部署
多亏了我们在上一章中检查的持续集成设置,我们现在可以从源代码中持续生成可部署的工件。
我们的下一个目标是将流水线从持续集成升级为集成加交付的流水线。为了说明,我们处于一个三阶段的工作流中:
也就是说,在成功的集成运行之后,我们触发交付阶段,执行以下操作:
-
启动一个普通的 EC2 实例
-
应用配置管理:
-
安装我们生成的
demo-app
RPM -
安装其他必要的软件包,将其转化为 web 服务器
-
-
测试应用的配置(使用Serverspec)
-
从配置好的实例生成 AMI(使用Packer)
-
从生成的 AMI 启动 EC2 实例
-
对新 EC2 实例运行额外的测试
该流水线将确保应用的 RPM 正确安装,配置管理按预期应用,我们的新 AMI 工件符合目标。最后,我们应该得到一个闪亮的、预先配置好的、生产就绪的 AMI,其中包含我们的demo-app
web 服务器。
为了完成这些任务,我们将引入两种新工具——Packer 和 Serverspec(详细信息将在后续章节介绍)。
由于我们是在此基础上构建的,我们将能够重用到目前为止的大部分工作。如同之前一样,我们将从准备代码、将其部署到 AWS 并配置 Jenkins Pipeline 开始。
如果你在上一章保持了 AWS 环境运行,可以跳过部分以下步骤。虽然我认为从头开始可能更好,以避免任何混淆。
准备 Terraform 模板
除了我们为 Jenkins 所需的常规 VPC、IGW 和子网外,我们还将为 demo-app
web 服务器场景部署 NAT 和 ELB。
资源
注意事项
我们从 VPC、IGW 和 NAT 开始:
resource "aws_vpc" "terraform-vpc" {
cidr_block = "${var.vpc-cidr}"
...
resource "aws_internet_gateway" "terraform-igw" {
vpc_id = "${aws_vpc.terraform-vpc.id}"
}
resource "aws_eip" "nat-eip" {
vpc = true
}
resource "aws_nat_gateway" "terraform-nat" {
allocation_id = "${aws_eip.nat-eip.id}"
subnet_id = "${aws_subnet.public-1.id}"
depends_on = ["aws_internet_gateway.terraform-igw"]
...
我们为 Jenkins 和 ELB 添加一个public
子网,并为 EC2 web 服务器添加一个private
子网:
resource "aws_route_table" "public" {
vpc_id = "${aws_vpc.terraform-vpc.id}"
...
resource "aws_route_table" "private" {
vpc_id = "${aws_vpc.terraform-vpc.id}"
...
接下来是 IAM。我们需要为 Jenkins 创建一个角色:
resource "aws_iam_role" "jenkins" {
name = "jenkins"
path = "/"
assume_role_policy = <<EOF
{
另一个用于demo-app
web 服务器的配置:
resource "aws_iam_role" "demo-app" {
name = "demo-app"
path = "/"
assume_role_policy = <<EOF
{
它们将共享一个公共策略,允许它们访问 CodeCommit(我们将基础设施和应用代码保存在这里)和 S3(我们存储 RPM 工件的地方):
resource "aws_iam_policy" "common" {
name = "common"
path = "/"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codecommit:Get*",
"codecommit:GitPull",
"codecommit:List*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"NotAction": [
"s3:DeleteBucket"
],
"Resource": "*"
...
新加入的 Packer 将需要一个单独的策略,以允许操作 EC2 资源。我们将使用它来启动/停止/终止实例并创建 AMI:
resource "aws_iam_policy" "jenkins" {
name = "jenkins"
path = "/"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:AttachVolume",
"ec2:CreateVolume",
"ec2:DeleteVolume",
"ec2:CreateKeypair",
"ec2:DeleteKeypair",
"ec2:DescribeSubnets"
...
"Resource": "*",
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": ["${aws_iam_role.demo-app.arn}"]
...
需要允许 PassRole
是一个 IAM 安全功能,有助于防止用户/服务授予自己超出应有权限的操作(参见:blogs.aws.amazon.com/security/post/Tx3M0IFB5XBOCQX/Granting-Permission-to-Launch-EC2-Instances-with-IAM-Roles-PassRole-Permission
)。
我们需要为 ELB 创建一个安全组,接受来自全球的 HTTP 流量:
resource "aws_security_group" "demo-app-elb" {
name = "demo-app-elb"
description = "ELB security group"
vpc_id = "${aws_vpc.terraform-vpc.id}"
ingress {
from_port = "80"
to_port = "80"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
...
然后是 ELB 本身:
resource "aws_elb" "demo-app-elb" {
name = "demo-app-elb"
security_groups = ["${aws_security_group.demo-app-elb.id}"]
subnets = ["${aws_subnet.public-1.id}"]
listener {
instance_port = 80
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
...
我们为 Jenkins 创建了一个安全组,允许来自任何地方的 SSH 和 HTTP/S 流量:
resource "aws_security_group" "jenkins" {
name = "jenkins"
description = "ec2 instance security group"
vpc_id = "${aws_vpc.terraform-vpc.id}"
ingress {
from_port = "80"
to_port = "80"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = "443"
to_port = "443"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
...
接下来的部分是针对 Web 服务器,接受来自 ELB 的 HTTP 请求以及来自 Jenkins 的 SSH 请求:
resource "aws_security_group" "demo-app" {
name = "demo-app"
description = "ec2 instance security group"
vpc_id = "${aws_vpc.terraform-vpc.id}"
ingress {
from_port = "80"
to_port = "80"
protocol = "tcp"
security_groups = ["${aws_security_group.demo-app-elb.id}"]
}
ingress {
from_port = "22"
to_port = "22"
protocol = "tcp"
security_groups = ["${aws_security_group.jenkins.id}"]
...
为了启动 Jenkins 节点,我们需要使用以前的用户数据,并做一个重要的修改:
resource "aws_instance" "jenkins" {
...
user_data = <<EOF
...
# Install SaltStack
yum -y install https://repo.saltstack.com/yum/amazon/salt-amzn-repo-latest-1.ami.noarch.rpm
yum clean expire-cache; yum -y install salt-minion; chkconfig salt-minion off
# Put custom minion config in place (for enabling masterless mode)
cp -r /srv/salt/minion.d /etc/salt/
echo -e 'grains:\n roles:\n - jenkins' > /etc/salt/minion.d/grains.conf
...
你会注意到,在我们安装 SaltStack 并配置好无主从节点(masterless minion)后,我们还添加了一个自定义的 Grains 文件。它包含的角色列表将帮助我们稍后分配 Salt States(因为我们现在将有两种不同类型的主机在配置管理下:jenkins
和我们的 demo-app
Web 服务器)。
变量
注意
与 第四章 相同,通过持续集成加速构建、测试和发布,我们只设置了几个与 VPC 和 EC2(Jenkins)相关的变量。
变量(值)
注意
和我们之前的部署一样,我们指定了 VPC 和 Jenkins 变量的值。
输出
注意
一些新的 outputs
反映了额外的 resources
。ELB 端点和我们私有子网的 ID 以及 demo-app
安全组:
output "ELB URI" {
value = "${aws_elb.demo-app-elb.dns_name}"
}
output "Private subnet ID" {
value = "${aws_subnet.private-1.id}"
}
output "Demo-app secgroup" {
value = "${aws_security_group.demo-app.id}"
}
这当然不是一个详尽无遗的列表,如果以后需要更多的信息,我们可以随时通过 terraform show
命令检索到我们部署的基础设施的详细描述。
准备 Salt 代码
我们将使用 SaltStack 在 Jenkins 和 demo-app
Web 服务器节点上应用配置管理。我们将使用 Grains 来定义哪些 States/Pillars 应用于哪些主机。让我们来看看代码:
状态
注意
top.sls
top
文件告诉我们,某些状态是所有主机/角色之间共享的,而其他状态则根据角色分配:
base:
'*':
- users
- yum-s3
'roles:jenkins':
- match: grain
- jenkins
- nginx.jenkins
- docker
- packer
'roles:demo-app':
- match: grain
- php-fpm
- nginx.demo-app
- demo-app
你已经熟悉了用户和 yum-s3
States。现在是时候为自己添加一个帐户和 SSH 密钥了。
jenkins
我们安装与之前相同的服务,并增加了一些额外的工具:
jenkins_prereq:
pkg.installed:
- pkgs:
...
- jq
- httpd-tools
...
我们将使用 jq
来解析 JSON 输出,并使用来自 httpd-tools
包的 ab
进行基本的 HTTP 负载测试。
nginx
这一次我们将 NGINX 状态分成三部分:
init.sls
这将安装主要的包并设置服务守护进程:
nginx:
pkg.installed: []
service.running:
- enable: True
- reload: True
- require:
- pkg: nginx
jenkins.sls
这将部署 NGINX 配置和为 Jenkins 服务所需的相关文件:
include:
- nginx
/etc/nginx/conf.d/jenkins.conf:
file.managed:
- source: salt://nginx/files/jenkins.conf
...
demo-app.sls
这将部署 NGINX 配置和为 demo-app
Web 服务器所需的相关文件:
include:
- nginx
/etc/nginx/conf.d/demo-app.conf:
file.managed:
- source: salt://nginx/files/demo-app.conf
在这两种情况下,我们都包含了 init.sls
,也就是 NGINX,它提供了共享功能,Docker 保持不变,而 Packer 是一个新的新增功能,我们很快就可以开始使用:
packer:
archive.extracted:
- name: /opt/
- source: 'https://releases.hashicorp.com/packer/0.10.1/packer_0.10.1_linux_amd64.zip'
- source_hash: md5=3a54499fdf753e7e7c682f5d704f684f
- archive_format: zip
- if_missing: /opt/packer
cmd.wait:
- name: 'chmod +x /opt/packer'
- watch:
- archive: packer
archive 模块方便地为我们下载并解压了 Packer 的 zip 文件。之后,我们通过 cmd.wait
确保二进制文件是可执行的,cmd.wait
在包更改时触发(即监视 archive)。
php-fpm
我们需要 PHP 才能提供我们的 PHP application
(demo-app
):
include:
- nginx
php-fpm:
pkg.installed:
- name: php-fpm
- require:
- pkg: nginx
service.running:
- name: php-fpm
- enable: True
- reload: True
- require_in:
- service: nginx
...
最后是 demo-app
状态,它安装了一个选定版本的应用程序 rpm
。我们稍后会讨论如何填充 /tmp/APP_VERSION
:
{% set APP_VERSION = salt'cmd.run' %}
include:
- nginx
demo-app:
pkg.installed:
- name: demo-app
- version: {{ APP_VERSION }}
- require_in:
- service: nginx
Pillars
注意
我们将重用上一章中的 nginx
和 users
Pillars。
Minion 配置
注意
虽然 masterless.conf
保持不变,但我们正在通过 UserData 为 Jenkins 设置一个自定义角色的 Grain,并通过配置文件为 demo-app
Web 服务器设置(稍后在章节中讨论)。
准备 Jenkins 代码
在我们继续进行 Jenkins 配置之前,允许我介绍两个新的助手——Packer 和 Serverspec。
Packer
注意
如所描述:
“Packer 是一个用于从单一源配置创建多个平台的机器和容器镜像的工具。” | ||
---|---|---|
--https://www.packer.io |
本质上,Packer 将为我们“打包”内容。我们将向它提供一个模板,基于该模板,它将启动一个 EC2 实例,执行请求的任务(通过 SSH),然后从中创建一个 AMI。Packer 可以与多个平台(如 AWS、GCE、OpenStack 等)进行交互,通过本地 shell、远程(SSH)、Salt、Ansible、Chef 等工具来配置资源。作为 HashiCorp 的产品,Packer 使用的模板系统与 Terraform 类似,这并不令人惊讶。
demo-app.json
在这里,我们定义了要配置的内容及其配置方式。首先,我们设置了我们的 variables
:
"variables": {
"srcAmiId": null,
"amiName": null,
"sshUser": null,
"instanceProfile": null,
"subnetId": null,
"vpcId": null,
"userDataFile": null,
"appVersion": null
}
...
我们已经将实际的值导出到一个 variables
文件中(稍后会提到)。在此将某个值设置为 null,将使其变为必填项。我们还可以在此处固定值或使用环境变量(参考 www.packer.io/docs/templates/user-variables.html
)。定义后,你可以通过以下语法引用变量:{{user `srcAmiId`}}
。
接下来的部分列出了 builders
,在我们的情况下是 AWS EC2:
"builders": [{
"type": "amazon-ebs",
"region": "us-east-1",
"source_ami": "{{user `srcAmiId`}}",
"instance_type": "t2.nano",
"ssh_username": "{{user `sshUser`}}",
"ami_name": "{{user `amiName`}}-{{timestamp}}",
"iam_instance_profile": "{{user `instanceProfile`}}",
"subnet_id": "{{user `subnetId`}}",
"vpc_id": "{{user `vpcId`}}",
"user_data_file": "{{user `userDataFile`}}",
"run_tags": {
"Name": "Packer ({{user `amiName`}}-{{timestamp}})",
"CreatedBy": "Jenkins"
},
"tags": {
"Name": "{{user `amiName`}}-{{timestamp}}",
"CreatedBy": "Jenkins"
}
}]
我们要求在 US-East-1 区域创建一个基于 EBS 的 nano 实例。该实例将通过 UserData 引导(稍后在文中介绍),并标记为 "CreatedBy": "Jenkins"
。
自然地,在启动实例后,我们希望对其进行配置:
"provisioners": [
{
"type": "shell",
"inline": [
"echo 'Waiting for the instance to fully boot up...'",
"sleep 30" ,
"echo "Setting APP_VERSION to {{user `appVersion`}}"",
"echo "{{user `appVersion`}}" > /tmp/APP_VERSION"
]
}
在这里,我们的第一个 provisioners
是一个通过 SSH 由 Packer 执行的 shell 命令(参考 www.packer.io/docs/provisioners/shell.html
)。它会暂停 30 秒,以便节点完成启动过程,然后创建 Salt php-fpm
State 所需的 APP_VERSION
文件。
接下来,我们运行 SaltStack:
{
"type": "salt-masterless",
"skip_bootstrap": true,
"local_state_tree": "salt/states",
"local_pillar_roots": "salt/pillars"
}
Packer 已经知道如何通过 salt-masterless provisioner
运行 Salt。它只需要 States 和 Pillars 的来源(参考: www.packer.io/docs/provisioners/salt-masterless.html
)。我们定义了 salt/
的相对路径,该路径是一个已检出的 Git 仓库的一部分(见 demo-app-cdelivery
)。我们选择通过 UserData 安装 Salt,因此设置了 skip_bootstrap: true
。
我们稍后会介绍 Serverspec,但这就是我们如何运行它的方式:
{
"type": "file",
"source": "serverspec",
"destination": "/tmp/"
},
{
"type": "shell",
"inline": [
"echo 'Installing Serverspec tests...'",
"sudo gem install --no-document rake serverspec",
"echo 'Running Serverspec tests...'",
"cd /tmp/serverspec && sudo /usr/local/bin/rake spec"
]
}
provisioners
文件用于在远程实例和 Packer 之间传输数据(参考 www.packer.io/docs/provisioners/file.html
)。我们将本地的 "serverspec/"
文件夹(其中包含我们的 Serverspec 测试)推送到远程的"/tmp"
目录。然后,运行一些 shell 命令来安装 Serverspec Ruby gem 并执行测试。
demo-app_vars.json
我们之前定义的变量值(另外,你也可以将其设置为一组-var 'key=value'
命令行参数):
{
"srcAmiId": "ami-6869aa05",
"amiName": "demo-app",
"sshUser": "ec2-user",
"instanceProfile": "demo-app",
"subnetId": "subnet-4d1c2467",
"vpcId": "vpc-bd6f0bda",
"userDataFile": "packer/demo-app_userdata.sh"
}
demo-app_userdata.sh
用于引导测试实例的 EC2 UserData:
#!/bin/bash
set -euf -o pipefail
exec 1> >(logger -s -t $(basename $0)) 2>&1
# Install SaltStack
yum -y install https://repo.saltstack.com/yum/amazon/salt-amzn-repo-latest-1.ami.noarch.rpm
yum clean expire-cache; yum -y install salt-minion; chkconfig salt-minion off
# Put custom grains in place
echo -e 'grains:\n roles:\n - demo-app' > /etc/salt/minion.d/grains.conf
与我们为 Jenkins 使用的类似。它安装了 SaltStack 并设置了角色 Grain。
Serverspec
注意
直接从首页开始:
“适用于通过 CFEngine、Puppet、Ansible、Itamae 或其他任何方式配置的服务器的 RSpec 测试。通过 Serverspec,您可以编写 RSpec 测试来检查您的服务器是否正确配置。Serverspec 通过本地执行命令、通过 SSH、WinRM、Docker API 等方式测试服务器的实际状态。所以您无需在服务器上安装任何代理软件,并且可以使用任何配置管理工具,Puppet、Ansible、CFEngine、Itamae 等等。但 Serverspec 的真正目标是帮助重构基础设施代码。” | ||
---|---|---|
--http://serverspec.org |
我们将使用 Serverspec 来验证在完成所有其他配置任务后 EC2 实例的最终状态。这有助于验证任何非配置管理的更改是否生效(例如,shell 命令),并确保配置管理已正确应用(例如,状态中没有竞争条件/重叠/冲突)。这确实会引入一些额外开销,有些人可能会质疑除了 SaltStack 执行外是否还需要它,因此这仍然是个人偏好。我将其视为第二层验证或安全网。
在 serverspec/
文件夹下的内容是通过运行 serverspec-init
创建的(请参考 serverspec.org
),选择 UNIX,然后选择 SSH。我们将示例的 spec.rb
文件替换为自己的:
spec/localhost/demo-app_spec.rb
require 'spec_helper'
versionFile = open('/tmp/APP_VERSION')
appVersion = versionFile.read.chomp
describe package("demo-app-#{appVersion}") do
it { should be_installed }
end
describe service('php-fpm') do
it { should be_enabled }
it { should be_running }
end
describe service('nginx') do
it { should be_enabled }
it { should be_running }
end
describe user('veselin') do
it { should exist }
it { should have_authorized_key 'ssh-rsa ...' }
end
Serverspec 对支持的资源类型执行测试(请参考 serverspec.org/resource_types.html
)。
在前面的简短示例中,我们断言:
-
我们的
demo-app
包的特定版本已被安装 -
PHP-FPM 和 NGINX 正在运行并在启动时启用
-
给定用户的 SSH
authorized_keys
文件包含预期内容
我们可以从包含文件夹运行 Serverspec 测试,方法如下:
cd /tmp/serverspec && sudo /usr/local/bin/rake spec
它将解析所有以 _spec.rb
结尾的文件。我们仅使用 sudo
,因为在这种情况下,我们尝试读取一个私密文件(authorized_keys
)。
回到 Jenkins。我们已经熟悉 Jenkinsfile
的概念(如我们的集成作业中使用的)。在这个示例中,我们将使用相同的方法添加第二个(交付)管道。
让我们来检查这两个管道作业。
demo-app
注意
这是我们旧的集成作业,下载应用代码,针对它运行测试,生成 RPM 包并将包上传到 YUM 仓库。我们将为这个过程添加一个新阶段:
stage "Trigger downstream"
build job: "demo-app-cdelivery",
parameters: [[$class: "StringParameterValue", name: "APP_VERSION", value:
"${gitHash}-1"]], wait: false
最后阶段触发了我们的下一个作业,即 Delivery 流水线,并将APP_VERSION
参数传递给它。
该参数的值是gitHash
,我们一直用它作为demo-app RPM 包
的版本字符串。
你看到的gitHash
后面附加的-1
表示 rpm 的次版本号,你现在可以放心忽略它。
将wait
设置为false
意味着我们不希望保持当前作业运行,等待随后触发的作业完成。
demo-app-cdelivery
注意
现在进入有趣的部分。Delivery 作业已传递APP_VERSION
并准备启动,让我们按照Jenkinsfile
中描述的流程进行操作。
我们从清理工作区开始,检出demo-app-cdelivery
仓库,然后在其上添加 SaltStack 代码。我们需要这两个代码库,以便启动一个实例并配置它作为 Web 服务器:
#!groovy
node {
step([$class: 'WsCleanup'])
stage "Checkout Git repo"
checkout scm
stage "Checkout additional repos"
dir("salt") {
git "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/salt"
}
之后,我们准备运行 Packer:
stage "Run Packer"
sh "/opt/packer validate -var="appVersion=$APP_VERSION" -var-file=packer/demo-app_vars.json packer/demo-app.json"
sh "/opt/packer build -machine-readable -var="appVersion=$APP_VERSION" -var-file=packer/demo-app_vars.json packer/demo-app.json | tee packer/packer.log"
首先,我们验证模板,然后执行,要求输出机器可读的结果。Packer 将启动一个实例,通过 SSH 连接,应用所有相关的 Salt States,运行 Serverspec 测试,并生成一个 AMI,这个 AMI 本质上是一个已经安装了demo-app
及其所有前置条件的 Web 服务器。
然后,我们继续启动第二个 EC2 实例;这次使用我们刚刚创建的 AMI:
stage "Deploy AMI"
def amiId = sh returnStdout: true, script:"tail -n1 packer/packer.log | awk '{printf \$NF}'"
def ec2Keypair = "terraform"
def secGroup = "sg-2708ef5d"
def instanceType = "t2.nano"
def subnetId = "subnet-4d1c2467"
def instanceProfile = "demo-app"
echo "Launching an instance from ${amiId}"
sh "aws ec2 run-instances \
--region us-east-1 \
--image-id ${amiId} \
--key-name ${ec2Keypair} \
--security-group-ids ${secGroup} \
--instance-type ${instanceType} \
--subnet-id ${subnetId} \
--iam-instance-profile Name=${instanceProfile} \
| tee .ec2_run-instances.log \
"
def instanceId = sh returnStdout: true, script: "printf \$(jq .Instances[0].InstanceId < .ec2_run-instances.log)"
顶部看到的变量是我们从 Terraform 获取的(terraform show
)。
我们使用aws cli
在私有 VPC 子网内启动实例,附加demo-app
安全组、Terraform 密钥和demo-app
实例配置文件。你会注意到,这里不需要传递任何 EC2 凭证,因为 Jenkins 已经通过我们之前分配给它的 IAM 角色获得授权。
接下来,我们通过解析aws cli
的 JSON 输出并使用jq
来获取instanceId
(参考stedolan.github.io/jq
)。
在我们启动实例之后,我们为其设置标签,注册到 ELB,并循环直到其 ELB 状态变为InService
:
sh "aws ec2 create-tags --resources ${instanceId} \
--region us-east-1 \
--tags Key=Name,Value="Jenkins (demo-app-$APP_VERSION)"
Key=CreatedBy,Value=Jenkins \ \
"
echo "Registering with ELB"
def elbId = "demo-app-elb"
sh "aws elb register-instances-with-load-balancer \
--region us-east-1 \
--load-balancer-name ${elbId} \
--instances ${instanceId} \
"
echo "Waiting for the instance to come into service"
sh "while [ "x\$(aws elb describe-instance-health --region us-east-1 --load-
balancer-name ${elbId} --instances ${instanceId} |
jq .InstanceStates[].State | tr -d '"')" != "xInService" ]; do : ; sleep 60;
done"
现在节点已经准备好服务,我们可以使用 AB 工具启动我们即兴设计的负载测试:
stage "Run AB test"
def elbUri = "http://demo-app-elb-1931064195.us-east-1.elb.amazonaws.com/"
sh "ab -c5 -n1000 -d -S ${elbUri} | tee .ab.log"
def non2xx = sh returnStdout: true, script:"set -o pipefail;(grep 'Non-2xx' .ab.log | awk '{printf \$NF}') || (printf 0)"
def writeErr = sh returnStdout: true, script:"grep 'Write errors' .ab.log | awk '{printf \$NF}'"
def failedReqs = sh returnStdout: true, script:"grep 'Failed requests' .ab.log | awk '{printf \$NF}'"
def rps = sh returnStdout: true, script:"grep 'Requests per second' .ab.log | awk '{printf \$4}' | awk -F. '{printf \$1}'"
def docLen = sh returnStdout: true, script:"grep 'Document Length' .ab.log | awk '{printf \$3}'"
echo "Non2xx=${non2xx}, WriteErrors=${writeErr}, FailedReqs=${failedReqs}, ReqsPerSec=${rps}, DocLength=${docLen}"
sh "if [ ${non2xx} -gt 10 ] || [ ${writeErr} -gt 10 ] || [ ${failedReqs} -gt 10 ] || [ ${rps} -lt 1000 ] || [ ${docLen} -lt 10 ]; then \
echo "ERR: AB test failed" | tee -a .error.log; \
fi \
"
在 AB 测试结束时,各种报告的指标将与预设的阈值进行比较并记录。
不再需要 EC2 实例,因此可以终止:
stage "Terminate test instance"
sh "aws ec2 terminate-instances --region us-east-1 --instance-ids ${instanceId}"
在最后阶段,任务的退出代码由 AB 测试结果决定:
stage "Verify test results"
sh "if [ -s '.error.log' ]; then \
cat '.error.log'; \
:> '.error.log'; \
exit 100; \
else \
echo 'Tests OK'; \
fi \
"
准备 CodeCommit 仓库
理想情况下,我们会将所有前述代码放入版本控制中,因此我们需要创建一些仓库。我们需要一个具有足够权限的 IAM 用户来执行此操作:
注意
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"NotAction": [
"codecommit:DeleteRepository"
],
"Resource": "*"
},
{
"Effect": "Allow",
"NotAction": [
"s3:DeleteBucket"
],
"Resource": "*"
},
{
"Sid": "Stmt1461764665000",
"Effect": "Allow",
"Action": [
"ec2:AllocateAddress",
"ec2:AssociateAddress",
...
我们创建一个具有前述策略的 terraform
IAM 用户,授予我们执行 CodeCommit 任务的权限,并且稍后也能进行 Terraform 部署(记得写下 API 密钥)。
请参阅上一章了解如何导出 API 密钥并创建三个 CodeCommit 仓库:salt
、demo-app
和 demo-app-cdelivery
。
你需要在本地克隆仓库,并分别用我们之前准备的代码填充每个仓库(参见:github.com/PacktPublishing/Implementing-DevOps-on-AWS/tree/master/5585_05_CodeFiles/CodeCommit
)。
部署 Terraform 模板
创建一个 terraform
EC2 密钥对,然后在 Terraform 模板文件夹中运行 terraform plan、terraform validate,最后运行 terraform apply(如果需要,请参考上一章了解如何执行这些操作)。
初始化 Jenkins
一旦 Terraform 完成部署,你将获得 Jenkins EIP 值输出。对其进行主机名查找并在浏览器中加载得到的地址。你应该能看到入门页面(截图和说明见上一章):
-
解锁 Jenkins
-
安装推荐的插件
-
创建管理员用户
配置 Jenkins 任务
在重新创建持续集成管道任务之前,我们需要一个用于 YUM 仓库的 S3 存储桶。创建一个存储桶(除非你保留了旧的存储桶),然后相应更新 demo-app/Jenkinsfile
脚本,提交并推送 Git 更改到上游。
demo-app 管道
请参阅上一章的设置管道步骤,创建持续集成任务。这次我们称之为 demo-app
。脚本路径保持不变(git-codecommit.us-east-1.amazonaws.com/v1/repos/demo-app
)。
你现在应该有这个:
由于我们还没有配置 YUM 仓库,管道将会失败:
第一个作业运行已经将代码库内容上传到 S3。现在,我们需要更新salt/states/yum-s3/files/s3.repo
文件,加入 S3 URL,并将仓库设置为enabled
。提交并推送 Salt 更改到 Git 仓库,然后在 Jenkins 节点上拉取并应用这些更改。
随后的管道运行让我们更进一步:
这次的失败是因为我们的下游作业尚未准备好。我们接下来修复这个问题。
demo-app-cdelivery 管道
从 Jenkins 的仪表盘中,我们选择新建项目:
我们将其称为demo-app-cdelivery
:
这个作业将由另一个作业触发,因此无需轮询 SCM。此外,我们将一个参数传递到这个管道中:
最后,我们设置了Jenkinsfile
的位置(git-codecommit.us-east-1.amazonaws.com/v1/repos/demo-app-cdelivery
):
你还记得我们在 Packer 的variables
文件中指定的 VPC 详情,以及在这个管道中的Jenkinsfile
吗?我们需要将它们设置为匹配当前的 VPC:
-
更新
packer/demo-app_vars.json
中的变量-
srcAmiId
可以是最新的 AmazonLinux AMI -
subnetId
是私有子网的 ID -
vpcId
-
-
更新
demo-app-cdelivery/Jenkinsfile
:-
在部署 AMI阶段:
-
secGroup
是demo-app
安全组的 ID -
subnetId
是前面提到的私有 VPC 子网的 ID
-
-
在运行 AB 测试阶段
elbUri
是demo-app-elb
ELB 的端点地址
-
-
提交并推送你的更改。
在这里,我们准备好了两个管道,准备开始执行:
让我们通过更改demo-app/src/index.php
中的$full_name
来触发demo-app
运行。你应该会在检测到 Git 更改后看到它运行。运行结束时,它应该会触发下游的demo-app-cdelivery
管道,再过大约 10 分钟,你应该能看到一个全新的demo-app AMI
等待你(查看 AWS 控制台)。
注意
请记得删除示例中使用的任何 AWS 资源(VPC、EC2、S3、IAM、CodeCommit 等),以避免不必要的费用。
概要
在本章中,我们扩展了 Jenkins 管道,以便在 VPC 环境中的 EC2 实例上部署和测试我们的应用程序工件。你学会了如何使用 Packer 模板化实例的配置,并且如何使用 Serverspec 进行额外的基础设施验证。
在下一章中,我们将通过添加持续部署元素来完成 Jenkins 管道的设置。我们将探讨如何将交付阶段创建的 AMI 部署到生产环境中。
第六章:持续部署 - 完全自动化工作流
欢迎来到 CI 流程的最后阶段——持续部署。
我们现在准备好将我们在持续交付步骤中生成的 AMI 部署到生产环境。
对于这个过程,我们将使用蓝绿部署方法。我们的生产环境将由 ELB 和两个自动扩展组(蓝色组和绿色组)组成:
如果我们假设蓝色组包含当前的生产节点,那么在部署时,我们会执行以下操作:
-
将 ELB 附加到绿色组
-
使用新的 AMI 扩展绿色组
-
检查错误
-
缩小蓝色组的规模,将流量有效地转移到新 AMI 实例上
由于我们是在现有的 CI 流水线基础上进行构建,因此我们只需要对上一章的代码进行一些小的修改。我们需要添加几个额外的 Terraform 资源;让我们来看看这些资源。
Terraform 代码 (resources.tf)
注意
我们添加了第二个公共子网和一个匹配的私有子网,以便能够将生产实例分布到多个可用区。
aws_subnet
资源创建一个名为 public-2
的子网。它需要一些属性,如 VPC ID、CIDR 块和可用区(AZ),这些值我们从变量中获取。为了计算 CIDR 和 AZ 值,我们使用 Terraform 的插值函数(参考:www.terraform.io/docs/configuration/interpolation.html
):
resource "aws_subnet" "public-2" {
vpc_id = "${aws_vpc.terraform-vpc.id}"
cidr_block = "${cidrsubnet(var.vpc-cidr, 8, 3)}"
availability_zone = "${element(split(",",var.aws-availability-zones), count.index + 1)}"
map_public_ip_on_launch = true
tags {
Name = "Public"
}
}
接下来,我们将新创建的子网与路由表关联:
resource "aws_route_table_association" "public-2" {
subnet_id = "${aws_subnet.public-2.id}"
route_table_id = "${aws_route_table.public.id}"
}
然后对Private
子网重复此操作:
resource "aws_subnet" "private-2" {
vpc_id = "${aws_vpc.terraform-vpc.id}"
cidr_block = "${cidrsubnet(var.vpc-cidr, 8, 4)}"
availability_zone = "${element(split(",",var.aws-availability-zones), count.index +1)}"
map_public_ip_on_launch = false
tags {
Name = "Private"
}
}
resource "aws_route_table_association" "private-2" {
subnet_id = "${aws_subnet.private-2.id}"
route_table_id = "${aws_route_table.private.id}"
}
在这个 VPC 中,我们最终会得到子网 1 和 3 为公共子网,子网 2 和 4 为私有子网。
下一步是添加生产 ELB 和为其配置的安全组:
resource "aws_security_group" "demo-app-elb-prod" {
name = "demo-app-elb-prod"
description = "ELB security group"
vpc_id = "${aws_vpc.terraform-vpc.id}"
ingress {
from_port = "80"
to_port = "80"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
注意协议值"-1"
,表示“所有”:
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_elb" "demo-app-elb-prod" {
name = "demo-app-elb-prod"
security_groups = ["${aws_security_group.demo-app-elb-prod.id}"]
subnets = ["${aws_subnet.public-1.id}", "${aws_subnet.public-2.id}"]
cross_zone_load_balancing = true
connection_draining = true
connection_draining_timeout = 30
listener {
instance_port = 80
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
tags {
Name = "demo-app-elb-prod"
}
}
让我们还更新demo-app
安全组的入站规则,以允许来自 ELB 的流量。为了更直观地展示,下面是我们之前的图示,并加上了更多标签:
代码如下:
resource "aws_security_group" "demo-app" {
name = "demo-app"
description = "ec2 instance security group"
vpc_id = "${aws_vpc.terraform-vpc.id}"
ingress {
from_port = "80"
to_port = "80"
protocol = "tcp"
security_groups = ["${aws_security_group.demo-app-elb.id}", "${aws_security_group.demo-app-elb-prod.id}"]
}
然后我们引入我们的蓝绿自动扩展组(ASG)和一个临时启动配置:
resource "aws_launch_configuration" "demo-app-lcfg" {
name = "placeholder_launch_config"
image_id = "${var.jenkins-ami-id}"
instance_type = "${var.jenkins-instance-type}"
iam_instance_profile = "${aws_iam_instance_profile.demo-app.id}"
security_groups = ["${aws_security_group.demo-app.id}"]
}
resource "aws_autoscaling_group" "demo-app-blue" {
name = "demo-app-blue"
launch_configuration = "${aws_launch_configuration.demo-app-lcfg.id}"
vpc_zone_identifier = ["${aws_subnet.private-1.id}", "${aws_subnet.private-2.id}"]
min_size = 0
max_size = 0
tag {
key = "ASG"
value = "demo-app-blue"
propagate_at_launch = true
}
}
resource "aws_autoscaling_group" "demo-app-green" {
name = "demo-app-green"
launch_configuration = "${aws_launch_configuration.demo-app-lcfg.id}"
vpc_zone_identifier = ["${aws_subnet.private-1.id}", "${aws_subnet.private-2.id}"]
min_size = 0
max_size = 0
tag {
key = "ASG"
value = "demo-app-green"
propagate_at_launch = true
}
}
这里的启动配置实际上只是一个占位符,以便我们可以定义自动扩展组(这也是为什么我们重复使用 Jenkins 变量)。稍后,我们将创建一个新的真实启动配置,用于作为流水线的一部分服务demo-app
。
outputs.tf
注意
对输出做了一些小的添加,以便给我们生产环境 ELB 端点:
output "ELB URI PROD" {
value = "${aws_elb.demo-app-elb-prod.dns_name}"
}
部署
现在是练习时间。使用之前提到的模板和来自github.com/PacktPublishing/Implementing-DevOps-on-AWS/tree/master/5585_06_CodeFiles
的其余熟悉代码,再加上你的先前经验,你应该能够创建一个 VPC 并启动一个 Jenkins 实例,配置两个流水线,正如我们在持续交付章节中所做的那样。别忘了更新任何与部署相关的细节,如下所示:
-
salt:states:users:files
中的 SSH 公钥 -
serverspec
测试规范中的授权密钥 -
salt:states:yum-s3:files:s3.repo
中的 S3 URI -
demo-app/Jenkinsfile
中的 S3 桶名称 -
packer:demo-app_vars.json
中的变量 -
demo-app-cdelivery/Jenkinsfile
中的变量
我建议你禁用demo-app任务中的 SCM 轮询,这样在所有下游任务配置完之前,我们不会触发运行。
假设一切顺利,我们回到之前的状态:
Jenkins 流水线
之前我们已经将集成和交付流水线串联起来,获取代码并生成 AMI 工件。我们的下一个任务是设计一个第三个流水线,将 AMI 部署到生产环境中。
在我们可以在 Jenkins 中创建新任务之前,我们需要通过 Git 使其代码可用:
注意
我们稍后将详细检查文件,目前请先创建并填充一个名为demo-app-cdeployment
的 CodeCommit 仓库。与我们其他仓库类似,新仓库的 URL 将是git-codecommit.us-east-1.amazonaws.com/v1/repos/demo-app-cdeployment
。
有了这些信息,我们继续创建流水线:
它将需要一个AMI ID
参数(从交付任务传递过来):
然后,当然,还需要Jenkinsfile
的位置(git-codecommit.us-east-1.amazonaws.com/v1/repos/demo-app-cdeployment
):
有了最后的任务,我们的 Jenkins 仪表板看起来是这样的:
持续部署流水线
回到代码,如之前所承诺:
注意
我们的 Jenkinsfile 非常简单:
#!groovy
node {
step([$class: 'WsCleanup'])
stage "Checkout Git repo" {
checkout scm
}
stage "Deploy AMI" {
sh returnStdout: false, script: "bash ./cdeployment.sh ${AMI_ID}"
}
}
我们只需要检查关联的代码库并执行一个 shell 脚本。自然,我们本可以用 Groovy 编写整个任务,但我个人更习惯于使用 Bash,因此最终使用了 cdeployment.sh
。
我们在本章开头简要描述了部署任务。一般来说,我们将从两个独立的实例集群中提供应用程序代码,并在它们之间切换流量。我们将使用功能强大且用户友好的 AWS CLI 执行大部分操作,并使用 Bash 处理任何输入/输出数据。
让我们深入了解脚本的更多细节。
cdeployment.sh
注意
在顶部,我们定义了 Auto Scaling Groups 的名称、生产环境的 ELB 以及我们将使用的 AMI ID(从上游管道传递过来):
#!/bin/bash
set -ef -o pipefail
blueGroup="demo-app-blue"
greenGroup="demo-app-green"
elbName="demo-app-elb-prod"
AMI_ID=${1}
一些辅助函数:
function techo() {
echo "[$(date +%s)] " ${1}
}
function Err() {
techo "ERR: ${1}"
exit 100
}
即,techo
(带时间戳的回显)以便输出更多信息,以及当我们遇到问题时的 ERR
。
如果我们需要中止部署并将基础设施恢复到原始状态,我们将使用此方法:
function rollback() {
techo "Metrics check failed, rolling back"
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${newActiveGroup} \
--min-size 0
techo "Instances ${1} entering standby in group ${newActiveGroup}"
aws autoscaling enter-standby --should-decrement-desired-capacity \
--auto-scaling-group-name ${newActiveGroup} --instance-ids ${1}
techo "Detaching ${elbName} from ${newActiveGroup}"
aws autoscaling detach-load-balancers --auto-scaling-group-name ${newActiveGroup} \
--load-balancer-names ${elbName}
Err "Deployment rolled back. Please check instances in StandBy."
}
在我们的案例中,如果我们检测到某些指标的错误计数增加,我们将中止部署。我们会将新部署的实例设置为 Standby 模式,然后将 ELB 从指定的 Auto Scaling Group 中分离。
每次我们启动新的实例时,都应该暂停,允许它们完全初始化,然后验证它们到目前为止所做的工作,下面的 wait_for_instances()
函数将帮助我们完成此任务。
等待预期数量的实例启动:
techo ">>> Waiting for instances to launch"
asgInstances=()
while [ ${#asgInstances[*]} -ne ${1} ];do
sleep 10
asgInstances=($(aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-name ${newActiveGroup} | jq .AutoScalingGroups[0].Instances[].InstanceId | tr -d '"' ))
techo "Launched ${#asgInstances[*]} out of ${1}"
done
等待它们变为可用:
techo ">>> Waiting for instances to become available"
asgInstancesReady=0
iterList=(${asgInstances[*]})
while [ ${asgInstancesReady} -ne ${#asgInstances[*]} ];do
sleep 10
for i in ${iterList[*]};do
asgInstanceState=$(aws autoscaling describe-auto-scaling-instances \
--instance-ids ${i} | jq .AutoScalingInstances[0].LifecycleState | tr -d '"')
if [[ ${asgInstanceState} == "InService" ]];then
asgInstancesReady="$((asgInstancesReady+1))"
iterList=(${asgInstances[*]/${i}/})
fi
done
techo "Available ${asgInstancesReady} out of ${#asgInstances[*]}"
done
让 ELB 将其声明为 InService
:
techo ">>> Waiting for ELB instances to become InService"
elbInstancesReady=0
iterList=(${asgInstances[*]})
while [ ${elbInstancesReady} -ne ${#asgInstances[*]} ];do
sleep 10
for i in ${iterList[*]};do
elbInstanceState=$(aws elb describe-instance-health \
--load-balancer-name ${elbName} --instances ${i} | jq .InstanceStates[].State | tr -d '"')
if [[ ${elbInstanceState} == "InService" ]];then
elbInstancesReady=$((elbInstancesReady+1))
iterList=(${asgInstances[*]/${i}/})
fi
done
techo "InService ${elbInstancesReady} out of ${#asgInstances[*]}"
done
接下来,由于我们知道将要使用的区域,我们提前设置它,以避免在每个 AWS CLI 命令中都需要附加区域:
export AWS_DEFAULT_REGION="us-east-1"
在进一步操作之前,我们确保有一个有效的 AMI ID
可供使用:
[[ ${AMI_ID} = ami-* ]] || Err "AMI ID ${AMI_ID} is invalid"
我们将使用两个 Auto Scaling Groups 和一个 ELB,我们检查每个组的属性并提取 ELB 名称:
blueElb=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names ${blueGroup} | \
jq .AutoScalingGroups[0].LoadBalancerNames[0] | tr -d '"')
greenElb=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names ${greenGroup} | \
jq .AutoScalingGroups[0].LoadBalancerNames[0] | tr -d '"')
接下来,我们确保只有一个组与生产环境 ELB 关联:
[[ "${blueElb}" != "${greenElb}" ]] || Err "Identical ELB value for both groups"
if [[ "${blueElb}" == "${elbName}" ]]; then
activeGroup=${blueGroup}
newActiveGroup=${greenGroup}
elif [[ "${greenElb}" == "${elbName}" ]]; then
activeGroup=${greenGroup}
newActiveGroup=${blueGroup}
fi
[ -n "${activeGroup}" ] || Err "Missing activeGroup"
[ -n "${newActiveGroup}" ] || Err "Missing newActiveGroup"
techo "Active group: ${activeGroup}"
techo "New active group: ${newActiveGroup}"
此时,我们已经确定了当前处理流量的两个组中的哪个(Active
)以及哪个将接管它(newActive
)。
理想情况下,在我们部署任何实例之前,newActive
应为空:
asgInstances=($(aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-name ${newActiveGroup} | jq .AutoScalingGroups[0].Instances[].InstanceId | tr -d '"' ))
[ ${#asgInstances[*]} -eq 0 ] || Err "Found instances attached to ${newActiveGroup}!"
如果是这样,我们可以继续获取 Active
组的一些统计信息:
activeDesired=$(aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-name ${activeGroup} | jq .AutoScalingGroups[0].DesiredCapacity)
activeMin=$(aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-name ${activeGroup} | jq .AutoScalingGroups[0].MinSize)
activeMax=$(aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-name ${activeGroup} | jq .AutoScalingGroups[0].MaxSize)
scaleStep=$(( (30 * ${activeDesired}) /100 ))
Desired
/Min
/Max
是标准的自动扩展值,我们最终会将它们转移到newActive
组。scaleStep
在这种情况下是服务中实例的 30%,这是我们希望在部署过程中引入的初始实例数量(允许它们接收实时流量)。
如果我们的Active
组为空,那就非常奇怪;如果它的数量较少,我们应该将scaleStep
四舍五入到至少 1:
[ ${activeDesired} -gt 0 ] || Err "Active group ${activeGroup} is set to 0 instances!"
[ ${scaleStep} -gt 0 ] || scaleStep=1
这些是先决条件;现在让我们开始部署,通过逐步扩展newActive
组。
我们需要一个启动配置。要创建一个,我们可以自己传递所有需要的参数,或者通过提供Active
组中的一个示例实例,让 EC2 自动复制大部分参数:
activeInstance=$(aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-name ${activeGroup} | jq .AutoScalingGroups[0].Instances[0].InstanceId | tr -d '"')
[[ ${activeInstance} = i-* ]] || Err "activeInstance ${activeInstance} is invalid"
launchConf="demo-app-${AMI_ID}-$(date +%s)"
aws autoscaling create-launch-configuration --launch-configuration-name ${launchConf} \
--image-id ${AMI_ID} --instance-id ${activeInstance}
如下所示,将新创建的启动配置附加到该组:
techo ">>> Attaching ${launchConf} to ${newActiveGroup}"
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${newActiveGroup} \
--launch-configuration-name ${launchConf}
如下所示添加 ELB:
techo ">>> Attaching ${elbName} to ${newActiveGroup}"
aws autoscaling attach-load-balancers --auto-scaling-group-name ${newActiveGroup} \
--load-balancer-names ${elbName}
按如下方式开始扩展:
techo ">>> Increasing ${newActiveGroup} capacity (min/max/desired) to ${scaleStep}"
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${newActiveGroup} \
--min-size ${scaleStep} --max-size ${scaleStep} --desired-capacity ${scaleStep}
等待片刻,直到实例启动:
wait_for_instances ${scaleStep}
我们的初始实例组现在应该已经部署,附加到生产 ELB,并开始服务流量。在我们启动更多新的 AMI 副本之前,我们应该检查到目前为止是否没有造成任何问题。为此,我们可以暂停部署几分钟,并检查一些指标,例如非 200 响应的数量、异常情况或每秒请求数。为了简单起见,在这个示例中,我们假设这已经完成;在现实生活中,你会查询你的监控系统,或者可能拉取 CloudWatch ELB/EC2 统计数据的样本。
如果没有检测到任何异常,我们进一步扩展newActive
组,以匹配Active
组的大小:
techo ">>> Checking error metrics"
sleep 5
doRollback=false
${doRollback} && rollback "${asgInstances[*]}"
techo ">>> Matching ${newActiveGroup} capacity (min/max/desired) to that of ${activeGroup}"
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${newActiveGroup} \
--min-size ${activeMin} --max-size ${activeMax} --desired-capacity ${activeDesired}
正如你所预期的,还需要做一次检查:
wait_for_instances ${activeDesired}
这一次,我们可以模拟一个问题并触发回滚:
techo ">>> Checking error metrics"
sleep 5
doRollback=true
${doRollback} && rollback "${asgInstances[*]}"
rollback
功能应该会处理剩下的部分。如果我们将doRollback
设置为false
,我们的部署将按计划继续,并通过缩小Active
组,将流量完全转移到newActive
组:
techo ">>> Reducing ${activeGroup} size to 0"
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${activeGroup} \
--min-size 0 --max-size 0 --desired-capacity 0
然后将 ELB 从其中移除:
techo ">>> Detaching ${elbName} from ${activeGroup}"
aws autoscaling detach-load-balancers --auto-scaling-group-name ${activeGroup} \
--load-balancer-names ${elbName}
现在,让我们看看我们的脚本如何运行。首先,我们应该通过手动扩展一个组(比如蓝色组)来模拟一个Active
组,并将生产 ELB 附加到该组:
稍等片刻,你应该能看到三个实例和蓝色的 ELB:
现在,让我们重新启用demo-app
作业的 SCM 轮询,并通过推送代码更改到其 CodeCommit 仓库来触发一个运行。你应该能看到管道在运行,并沿途调用下游的两个管道。
如果你选择模拟一个指标问题并导致回滚,那么已部署的实例应该进入Standby模式:
在这种情况下,rollback
是在一个实例初次部署后触发的(scaleStep=1
)。理论上,下一步应该是检查该实例,寻找可能导致错误指标的原因。
如果实例被认为是健康的,那么我们需要通过将实例投入使用,进一步扩展该组,然后缩小另一个组来手动完成部署(本质上完成 cdeployment
脚本中的剩余步骤)。
否则,实例可以投入使用,然后将组缩减为零,使基础设施恢复到其原始状态,蓝色组保持为 Active
。
如果你选择不进行任何回滚,部署应该按计划进行,最终绿色组会接管蓝色组,表示部署成功:
此时,如果你在浏览器中加载 ELB URI,你应该会收到来自我们新部署的 AMI 提供的 demo-app
的响应。
恭喜!
总结
在本章中,我们通过添加部署组件完成了我们的 Jenkins CI 解决方案。我们广泛使用了 AWS CLI 来编排蓝绿部署过程。生成的流水线或此类集合使我们能够持续集成应用程序的代码更改,并构建包含这些更改的 AMI,之后在通过某些测试并满足标准后,将其部署到指定的环境中。
下一章将带我们进入一个新方向,介绍监控、指标和日志收集的主题。我们将看看一些工具,帮助我们随时了解基础设施的状态,直观地呈现性能,并对问题作出反应。
第七章:指标、日志收集和监控
就是这样。本章本来可以在这里结束,但为了那些希望更详细了解的朋友们,我将继续进行下去。
许多 DevOps 实践围绕着能够随时回顾和应对基础设施状态的理念——如果您需要的话。
这并不是说每当主机上的日期发生变化时都设置电子邮件通知,而是提供一系列合理、可用的事件数据流,使得操作员能够在压力和/或不确定的情况下做出合理的决策。
如果您一直在关注生活,您会注意到许多智者提到过平衡,黄金中庸。
您应该目标是以一种方式配置您的监控系统,以便能够及时接收到潜在重要事件的通知。这些通知应该以一种难以忽视的格式到达,并且提供足够的细节,让操作员能够做出合理的判断,了解发生了什么。
与此同时,所述监控系统必须尽量减少告警疲劳(如这篇简洁的 Datadog 文章所概述:www.datadoghq.com/blog/monitoring-101-alerting
)。
不幸的是,找到适合您情况(您的基础设施以及负责维护它的人们)的中间地带是一个冒险,您必须独自去完成。然而,我们可以一起花一些时间讨论一些工具,这些工具可以使这一过程更加愉快!
检查清单很复杂,所以这是其中之一:
-
集中日志记录:
-
使用Logstash和Elasticsearch进行日志的摄取和存储
-
使用 Elasticsearch 的Filebeat收集日志
-
使用Kibana可视化日志
-
-
指标:
-
使用Prometheus摄取和存储指标
-
使用Telegraf收集操作系统和应用程序的指标
-
使用Grafana可视化指标
-
-
监控:
-
使用Prometheus进行告警
-
使用Prometheus和Jenkins进行自我修复
-
自然,我们需要一些主机来作为前面所有检查清单的“游乐场”。在之前的章节中,我们已经充分练习了在 AWS 上部署 VPC EC2 实例,因此我在此行使委托权,并假设以下内容的存在:
-
一个带有 IGW、NAT 网关、2 个私有子网和 2 个公共子网的 VPC
-
2 个独立的、原生的 Amazon Linux EC2 实例(比如
t2.small
),位于公共子网内 -
1 个自动伸缩组(
t2.nano
)位于私有子网内 -
1 个面向互联网的 ELB,将 HTTP 流量传递到自动伸缩组
集中日志记录
自古以来,人类就努力将有限的注意力集中在生活中真正重要的事情上,并尽量不费力地找到它——如果可能的话。因此,我们从复制日志文件开始,进化到了集中式 (r)syslog,今天(我们从错误中学习)我们拥有了 Logstash 和 Elasticsearch。
Elasticsearch 是一个分布式的开源搜索和分析引擎,旨在实现水平扩展性、可靠性和易于管理。它通过一个复杂的、开发者友好的查询语言结合了搜索速度和分析能力,涵盖了结构化、非结构化和时间序列数据。**Logstash 是一个灵活的开源数据收集、增强和传输管道。Logstash 提供了与常见基础设施的连接器,便于集成,旨在高效处理越来越多的日志、事件和非结构化数据源,并将其分发到包括 Elasticsearch 在内的多种输出中。 | ||
---|---|---|
--www.elastic.co/products |
使用 Logstash 和 Elasticsearch 获取和存储日志
我们将使用 Logstash 接收、处理并将日志事件存储到 Elasticsearch 中。
本章中的演示目的,我们将在主机上手动安装和配置服务。实验完成后,当然应该使用配置管理工具来代替(眨眼)。
我们首先在一台独立的 EC2 实例上安装这两个服务(我们将其称为 ELK):
# yum -y install https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/rpm/elasticsearch/2.4.1/elasticsearch-2.4.1.rpm https://download.elastic.co/logstash/logstash/packages/centos/logstash-2.4.0.noarch.rpm
编辑 /etc/elasticsearch/elasticsearch.yml
:
注意
cluster.name: wonga-bonga
index.number_of_shards: 1
index.number_of_replicas: 0
index :
refresh_interval: 5s
为 Elasticsearch 集群选择一个唯一的名称非常重要,以确保节点不会意外地加入其他人的集群(如果您的局域网上有的话)。对于开发环境,我们只需要一个分片且没有副本。为了提高效率,我们设置 Elasticsearch 索引的刷新率为 5 秒。
创建一个 Logstash patterns
文件夹:
# mkdir /opt/logstash/patterns
创建一个示例 NGINX 模式 /opt/logstash/patterns/nginx
(参考:www.digitalocean.com/community/tutorials/adding-logstash-filters-to-improve-centralized-logging
):
注意
NGUSERNAME [a-zA-Z\.\@\-\+_%]+
NGUSER %{NGUSERNAME}
NGINXACCESS %{IPORHOST:clientip} %{NGUSER:ident} %{NGUSER:auth} \[%{HTTPDATE:timestamp}\] "%{WORD:verb} %{URIPATHPARAM:request} HTTP/%{NUMBER:httpversion}" %{NUMBER:response} (?:%{NUMBER:bytes}|-) (?:"(?:%{URI:referrer}|-)"|%{QS:referrer}) %{QS:agent}
创建 /etc/logstash/conf.d/main.conf
:
注意
input {
beats {
port => 5044
}
}
filter {
if [type] == "nginx-access" {
grok {
match => { "message" => "%{NGINXACCESS}" }
}
}
}
output {
elasticsearch {
hosts => "localhost:9200"
manage_template => false
index => "%{[@metadata][beat]}-%{+YYYY.MM.dd}"
document_type => "%{[@metadata][type]}"
}
}
Logstash 允许我们配置一个或多个监听器(输入),以接收数据,配置过滤器帮助我们处理数据,并设置输出,指定处理后数据应转发到哪里。
我们期望 Elasticsearch Filebeat 通过 TCP: 5044
传送日志。如果日志事件的类型是 nginx-access
,我们会根据 NGINXACCESS
模式对其进行修改,然后将其通过 TCP: 9200
发送到本地主机的 Elasticsearch 进行存储。
最后,让我们启动服务:
# service elasticsearch start
# service logstash start
使用 Elasticsearch Filebeat 收集日志
我们已经搭建好系统;接下来让我们推送一些来自 ELK 节点的数据。
我们将使用 Filebeat 收集本地感兴趣的日志,并将其转发到 Logstash(顺便说一下,这也是本地的):
Filebeat 是一个日志数据传输工具。它作为代理安装在服务器上,监控日志目录或特定日志文件,跟踪文件并将其转发到 Elasticsearch 或 Logstash 进行索引。 | ||
---|---|---|
--https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html |
安装:
# yum -y install https://download.elastic.co/beats/filebeat/filebeat-1.3.1-x86_64.rpm
虽然提供了直接发送到 Elasticsearch 的功能,但我们计划使用 Logstash,因此我们需要在/etc/filebeat/filebeat.yml
中禁用 Elasticsearch 输出,并启用 Logstash 输出。
注意
output:
#elasticsearch:
# hosts: ["localhost:9200"]
logstash:
hosts: ["localhost:5044"]
我们还可以列出更多要收集的日志文件:
filebeat:
prospectors:
-
paths:
- /var/log/*.log
- /var/log/messages
- /var/log/secure
然后启动服务:
# service filebeat start
很有趣,但让我们再启动一些 EC2 实例,享受更多乐趣!
我们将使用之前提到的自动扩展组。我们将在每个实例上安装 Filebeat,并配置它将选定的日志转发到我们的 Logstash 节点。
首先,确保 Logstash 实例的安全组允许来自自动扩展组的入站连接(TCP: 5044
)。
接下来,我们使用 EC2 用户数据脚本将 Filebeat 二进制文件和配置引导到自动扩展组中的每个 EC2 实例上(我们将其称为 Web 服务器):
注意
#!/bin/bash
yum -y install https://download.elastic.co/beats/filebeat/filebeat-1.3.1-x86_64.rpm
yum -y install nginx
cat << EOF > /etc/filebeat/filebeat.yml
filebeat:
prospectors:
-
paths:
- /var/log/*.log
- /var/log/messages
- /var/log/secure
-
paths:
- /var/log/nginx/access.log
document_type: nginx-access
registry_file: /var/lib/filebeat/registry
output:
logstash:
hosts: ["10.0.1.132:5044"]
EOF
service nginx start
service filebeat start
配置好后,继续扩展该组。新的 Web 服务器实例应立即开始流式传输日志。
使用 Kibana 可视化日志
我们已将日志收集到 Filebeat,并存储在 Elasticsearch 中,那我们来浏览这些日志吧?
Kibana,准时到达:
Kibana 是一个开源的分析和可视化平台,旨在与 Elasticsearch 配合使用。你可以使用 Kibana 搜索、查看和交互 Elasticsearch 索引中的数据。你可以轻松地执行高级数据分析,并在各种图表、表格和地图中可视化数据。 | ||
---|---|---|
--https://www.elastic.co/guide/en/kibana/current/introduction.html |
安装包:
# yum -y install https://download.elastic.co/kibana/kibana/kibana-4.6.1-x86_64.rpm
启动服务:
# service kibana start
默认端口是 TCP:5601
,如果在相关的安全组中允许,你应该能够看到 Kibana 仪表板:
将 索引模式 设置为 filebeat-* 然后点击 创建。
Kibana 现在已经准备好显示我们的 Filebeat 数据。切换到 发现 标签页查看最近的事件列表:
除了标准的 Syslog 消息外,你还会注意到一些 NGINX 访问日志 条目,字段根据我们之前指定的过滤条件进行了填充:
日志:完成。那么,关于一些度量呢?
度量
为了获取、存储和警报我们的度量,我们将探索另一个非常流行的开源项目,名为 Prometheus:
Prometheus 是一个开源的系统监控和警报工具包,最初在 SoundCloud 开发。Prometheus 的主要特点包括:- 多维数据模型(通过指标名称和键/值对标识时间序列)- 一个灵活的查询语言来利用这种维度- 无需分布式存储;单个服务器节点是自治的- 通过 HTTP 拉取模型进行时间序列采集- 通过中间网关支持推送时间序列- 通过服务发现或静态配置发现目标- 支持多种图形和仪表板模式 | ||
---|---|---|
--https://prometheus.io/docs/introduction/overview/emphasis> |
尽管它是一个几乎涵盖所有功能的系统,但该项目仍然遵循流行的 UNIX 模块化开发哲学。Prometheus 由多个组件组成,每个组件提供特定功能:
- 主要的 Prometheus 服务器,用于抓取和存储时间序列数据- 用于仪表化应用代码的客户端库- 支持短生命周期作业的推送网关- 基于 Rails/SQL 的 GUI 仪表板构建器- 特殊用途的导出器(如 HAProxy、StatsD、Ganglia 等)- 一个(实验性的)告警管理器- 命令行查询工具 | ||
---|---|---|
--https://prometheus.io/docs/introduction/overview/ |
使用 Prometheus 获取和存储度量
我们的第二个 EC2 实例将会托管 Prometheus 服务和 Jenkins(稍后会介绍),因此一个合适的名称可以是 promjenkins。
首先,下载并解压 Prometheus 和 Alertmanager 到 /opt/prometheus/server
和 /opt/prometheus/alertmanager
(参考:prometheus.io/download
)。
我们为 Alertmanager 创建一个基本的配置文件,存放在 /opt/prometheus/alertmanager/alertmanager.yml
(根据需要替换电子邮件地址):
注意
global:
smtp_smarthost: 'localhost:25'
smtp_from: 'alertmanager@example.org'
route:
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 1h
receiver: team-X-mails
receivers:
- name: 'team-X-mails'
e-mail_configs:
- to: 'team-X+alerts@example.org'
require_tls: false
这将简单地通过电子邮件发送警报通知。
启动服务:
# cd /opt/prometheus/alertmanager
# (./alertmanager 2>&1 | logger -t prometheus_alertmanager)&
确保默认的 TCP:9093
已被允许,然后你应该能够访问 http://$public_IP_of_promjenkins_node:9093/#/status
仪表盘:
返回到 Prometheus 服务器,默认的 /opt/prometheus/server/prometheus.yml
目前就足够了。我们可以启动该服务:
# cd /opt/prometheus/server
# (./prometheus -alertmanager.url=http://localhost:9093 2>&1 | logger -t prometheus_server)
打开 TCP:9090
,然后尝试 http://$public_IP_of_promjenkins_node:9090/status
:
我们已经准备好开始添加要监控的主机。也就是说,添加 Prometheus 要拉取的目标。
Prometheus 提供了多种定义目标的方式。最适合我们案例的是 ec2_sd_config
(参考:prometheus.io/docs/operating/configuration/#<ec2_sd_config>
)。我们所需要做的就是提供一组具有只读 EC2 访问权限的 API 密钥(AmazonEC2ReadOnlyAccess IAM 策略),然后 Prometheus 将为我们执行主机发现(参考:www.robustperception.io/automatically-monitoring-ec2-instances
)。
我们将 ec2_sd_config
配置追加到:/opt/prometheus/server/prometheus.yml
:
注意
- job_name: 'ec2'
ec2_sd_configs:
- region: 'us-east-1'
access_key: 'xxxx'
secret_key: 'xxxx'
port: 9126
relabel_configs:
- source_labels: [__meta_ec2_tag_Name]
regex: ^webserver
action: keep
我们只关注位于 us-east-1
区域,并且名称符合 ^webserver
正则表达式的实例。
现在让我们将其中一些实例上线。
使用 Telegraf 收集操作系统和应用程序的度量数据
我们将在 Prometheus 中使用拉取方式进行度量数据收集。这意味着我们的客户端(目标)将暴露它们的度量数据,供 Prometheus 拉取。
为了暴露操作系统度量数据,我们将部署 InfluxData 的 Telegraf(参考:github.com/influxdata/telegraf
)。
它提供了一套丰富的插件,能够提供大量度量。如果你需要更多的度量,可以选择自己编写插件(使用 Go)或使用 exec
插件,它本质上会尝试启动你指定的任何类型的脚本。
至于应用程序度量,我们有两个(至少)选择:
-
在应用程序中构建一个度量 API 端点
-
让应用程序将度量数据提交给外部守护进程(以 StatsD 为例)
顺便提一下,Telegraf 内置了一个 StatsD 监听器,因此如果你的应用程序已经有 StatsD 插桩,你应该能够直接将其指向 Telegraf。
继 ELK 示例之后,我们将扩展 EC2 用户数据脚本,以便在自动扩展组实例上安装 Telegraf。
注意
我们附加:
yum -y install https://dl.influxdata.com/telegraf/releases/telegraf-1.0.1.x86_64.rpm
cat << EOF > /etc/telegraf/telegraf.conf
[global_tags]
[agent]
interval = "10s"
round_interval = true
metric_batch_size = 1000
metric_buffer_limit = 10000
collection_jitter = "0s"
flush_interval = "10s"
flush_jitter = "0s"
precision = ""
debug = false
quiet = false
hostname = ""
omit_hostname = false
[[outputs.prometheus_client]]
listen = ":9126"
[[inputs.cpu]]
percpu = true
totalcpu = true
fielddrop = ["time_*"]
[[inputs.disk]]
ignore_fs = ["tmpfs", "devtmpfs"]
[[inputs.diskio]]
[[inputs.kernel]]
[[inputs.mem]]
[[inputs.processes]]
[[inputs.swap]]
[[inputs.system]]
EOF
service telegraf start
这里最重要的是 outputs.prometheus_client
,它将 Telegraf 转换为 Prometheus 抓取目标。如果你希望在此测试中启用更多度量,请务必检查默认配置文件(参考:github.com/influxdata/telegraf/blob/master/etc/telegraf.conf
)
接下来,检查 TCP: 9126
是否被允许进入自动扩展组安全组,然后启动几个节点。几秒钟后,你应该能在目标仪表板中看到任何匹配的实例(参考:http://$ public_IP_of_promjenkins_node:9090/targets
):
我们在先前配置的ec2抓取任务下看到了新主机。
使用 Grafana 可视化度量
的确,Prometheus 完全能够可视化我们现在从目标收集的数据,如下所示:
事实上,这就是推荐的做法,适用于你可能想要执行的任何临时查询。
如果你对仪表板有兴趣的话,你一定会非常欣赏 Grafana - 第八大奇迹(参考:grafana.org
)
查看这个网址以了解这个工具的使用感受:http://play.grafana.org
我的意思是,你知道多少个项目有 play URL 吗?!
-
所以,是的,Grafana,让我们在 promjenkins 节点上安装该服务:
# yum -y install https://grafanarel.s3.amazonaws.com/builds/ grafana-3.1.1-1470047149.x86_64.rpm # service grafana-server start
默认的 Grafana 端口是 TCP:
3000
,认证为admin:admin
。更新相关安全组后,我们应该能看到以下页面:http://$ public_IP_of_promjenkins_node:3000
: -
登录后,首先我们需要为我们的仪表板创建一个数据源:
-
在首页,选择创建一个新仪表板,然后使用左侧的绿色按钮 添加面板,接着选择 图表:
-
然后,添加一个基本的 CPU 使用率图表如下所示:
此时,我鼓励你浏览
docs.grafana.org
以了解更多有关模板、动态仪表板、访问控制、标签、脚本、播放列表等方面的信息。
监控
我们的指标已经流入 Prometheus。我们也有了一种探索和可视化它们的方法。下一步可能是配置某种告警,这样我们就能向其他人展示我们正在进行实际工作。
使用 Prometheus 进行告警
告警概述Prometheus 的告警功能分为两个部分。Prometheus 服务器中的告警规则将告警发送到 Alertmanager。Alertmanager 然后管理这些告警,包括静默、抑制、聚合和通过电子邮件、PagerDuty 和 HipChat 等方式发送通知。设置告警和通知的主要步骤是:- 设置并配置 Alertmanager- 配置 Prometheus 以通过 --alertmanager.url 标志与 Alertmanager 通信**- 在 Prometheus 中创建告警规则 | ||
---|---|---|
--https://prometheus.io/docs/alerting/overview/ |
让我们将其分解:
我们已经在 /opt/prometheus/alertmanager/alertmanager.yml
中以一些最小配置运行了 Alertmanager。
由于我们传递了 -alertmanager.url=http://localhost:9093
标志,我们的 Prometheus 实例已经意识到了这一点。
剩下的就是创建告警规则。我们将这些存储在 rules/
文件夹中:
# mkdir /opt/prometheus/server/rules
我们需要告诉 Prometheus 这个位置,因此我们需要在 prometheus.yml
中添加一个 rule_files
部分:
注意
rule_files:
- "rules/*.rules"
这样我们可以存储单独的规则文件,也许可以根据它们包含的规则类型来区分?
作为示例,让我们设置一个保活和磁盘使用告警:
注意
/opt/prometheus/server/rules/keepalive.rules
:
ALERT Keepalive
IF up == 0
FOR 1m
ANNOTATIONS {
summary = "Instance {{$labels.instance}} down",
description = "{{$labels.instance}} of job {{$labels.job}} has been down for more than 1 minute."
}
/opt/prometheus/server/rules/disk.rules
:
ALERT High_disk_space_usage
IF disk_used_percent > 20
FOR 1m
ANNOTATIONS {
summary = "High disk space usage on {{ $labels.instance }}",
description = "{{ $labels.instance }} has a disk_used value of {{ $value }}% on {{ $labels.path }})",
}
正如你所注意到的,我们在 FOR 1m
和 >20
上显得不耐烦,这意味着在检测到告警后仅经过 60 秒,且告警阈值只有 20% 的空间使用。
在更实际的场景中,我们应等待更长时间,以过滤掉任何瞬态问题,并使用严重性来区分关键警报和警告(参考:github.com/prometheus/alertmanager
)。
重新加载 Prometheus 并应用新规则。现在,假设其中一个 Web 服务器节点出现故障:
切换到警报标签页,我们看到:
在 Alertmanager 中,分别为:(http://$ public_IP_of_promjenkins_node:9093/#/alerts
):
此时,应该已经发送了电子邮件通知。
使用 Prometheus 和 Jenkins 的自我修复
每个运维人员的梦想是一个能够自我照看的生态系统。
想象一下,如果我们生活在一个环境中,不是接收到要求采取行动的警报,而是接收到仅仅是通知或我们已采取的行动的报告,会是怎样的?
例如,不再是“CRITICAL: 服务 X 未响应,请检查。”,而是“INFO: 服务 X 在 nn:nn:nn 时未响应,并在 nn:nn:nn 时经过 N 秒后重启。”
从技术上讲,如果我们为今天使用的工具提供足够的上下文,那么实现这一点应该不会太困难。在相同条件下,解决方案通常以相同方式解决警报,这些警报应被视为自动化的主要候选项。
为了演示,我们假设我们继承了这个旧的、不再支持的应用程序。总体来说是个不错的应用,但它没有整理自己的习惯,因此偶尔会填满其tmp
目录。
假设一下,虽然我们并不特别喜欢每天在随机的时间连接到这个应用的服务器去删除tmp
文件,但我们的朋友——Jenkins 先生,完全不介意。
方便的是,Jenkins 允许通过相关的JOB_URL
触发任务,同时 Prometheus 支持 Webhook 调用作为警报通知的方法。
计划如下:
-
每当触发
disk_space
警报时,Prometheus 将调用 Webhook 并传递警报详细信息作为参数给 Jenkins。 -
Jenkins 将使用这些参数来决定连接哪个主机,并清理应用程序的
tmp
目录。
我们需要:
-
创建一个参数化的 Jenkins 任务,可以通过远程触发。
-
允许 Jenkins 通过
ssh
连接到应用程序的主机。 -
在 Prometheus 中设置一个 Webhook 接收器,当某个警报触发时调用 Jenkins 任务。
首先在promjenkins
节点上快速安装 Jenkins:
# yum install http://mirrors.jenkins-ci.org/redhat-stable/
jenkins-2.7.1-1.1.noarch.rpm
# service jenkins start
TCP: 8080
需要开放,然后你应该能够通过http://$public_IP_of_promjenkins_node:8080
访问 Jenkins 服务。
在管理 Jenkins | 管理用户中为 Prometheus 创建一个账户:
然后,在管理 Jenkins | 配置全局安全下,选择 Jenkins 自有的用户数据库和基于矩阵的安全,然后添加两个账户。
提示
如果您发现这会导致在向 Jenkins 发出 curl
请求时出现问题,请取消选中防止跨站请求伪造攻击。
授予自己总体管理权限和Prometheus 总体读取权限,以及任务构建/读取权限:
为了能够 SSH 进入应用程序(Web 服务器)节点,我们需要为 Jenkins 用户生成一个密钥:
# su - -s /bin/bash jenkins
$ ssh-keygen -trsa -b4096
Generating public/private rsa key pair.
Enter file in which to save the key (/var/lib/jenkins/.ssh/id_rsa):
Created directory '/var/lib/jenkins/.ssh'
...
在这里,我们创建一个 Jenkins 用户的 SSH 配置文件(~/.ssh/config
),内容为:
Host 10.0.*
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
User ec2-user
这是为了允许我们的非交互式任务首次通过 SSH 连接到实例。
我们还需要将生成的公钥添加到自动扩展组的用户数据中,这样它就会出现在我们的 Web 服务器实例上。我们将使用标准的(Amzn-Linux)ec2-user 账户进行连接:
注意
...
# Add Jenkins's key
cat << EOF >> /home/ec2-user/.ssh/authorized_keys
{{JENKINS_PUB_KEY_GOES_HERE}}
EOF
现在,让我们使用一些参数创建 Jenkins 任务(自由风格项目):
我们稍后会讨论这四个参数(alertname
,alertcount
,instance
,labels
)。在构建部分,选择执行 Shell并输入exit 0
作为占位符,直到我们准备好进一步配置该任务。保存并让我们回到 Prometheus。
正如我们之前提到的,我们将使用 webhook 接收器来触发 Jenkins 任务。虽然接收器允许我们设置要调用的 URL,但似乎不允许包含任何参数。为此,我们将使用一个名为prometheus-am-executor的小型帮助程序应用程序(参考:github.com/imgix/prometheus-am-executor
)。
执行程序位于 Alertmanager 和任意可执行文件之间。它接收来自 Alertmanager 的 webhook 调用并运行该可执行文件,将警报变量列表传递给它。在我们的案例中,我们将执行一个 Shell 脚本,该脚本处理这些变量并构建一个 Jenkins 期望的curl
调用格式。
让我们在安装 Prometheus 和 Alertmanager 时同时安装帮助程序应用程序:
# yum -y install golang
# mkdir /opt/prometheus/executor && export GOPATH=$_
# go get github.com/imgix/prometheus-am-executor
成功后,您应该会在/opt/prometheus/executor/bin
目录中找到一个二进制文件。现在提到的脚本(可执行文件)是:
注意
#!/bin/bash
if [[ "$AMX_STATUS" != "firing" ]]; then
exit 0
fi
main() {
for i in $(seq 1 "$AMX_ALERT_LEN"); do
ALERT_NAME=AMX_ALERT_${i}_LABEL_alertname
INSTANCE=AMX_ALERT_${i}_LABEL_instance
LABELS=$(set|egrep "^AMX_ALERT_${i}_LABEL_"|tr '\n' ' '|base64 -w0)
PAYLOAD="{'parameter': [{'name':'alertcount', 'value':'${i}'}, {'name':'alertname', 'value':'${!ALERT_NAME}'}, {'name':'instance', 'value':'${!INSTANCE}'}, {'name':'labels', 'value':'${LABELS}'}]}"
curl -s -X POST http://localhost:8080/job/prometheus_webhook/build --user 'prometheus:password' --data-urlencode json="${PAYLOAD}"
done
wait
}
main "$@"
本质上,我们正在构建一个 HTTP 调用,访问我们 Jenkins 作业的 URL http://localhost:8080/job/prometheus_webhook/build
,并传递 alertcount
、alertname
、instance
和 labels
参数。所有值都来自 prometheus-am-executor 暴露的 AMX 环境变量(参考:github.com/imgix/prometheus-am-executor
)。
现在我们需要重新配置 Alertmanager 来使用 Webhook:
注意
global:
smtp_smarthost: 'localhost:25'
smtp_from: 'alertmanager@example.org'
route:
group_by: ['alertname', 'cluster', 'service']
group_wait: 10s
group_interval: 30s
repeat_interval: 1m
receiver: team-X-mails
routes:
- receiver: 'jenkins-webhook'
match:
alertname: "High_disk_space_usage"
receivers:
- name: 'team-X-mails'
e-mail_configs:
- to: 'veselin+testprom@kantsev.com'
require_tls: false
send_resolved: true
- name: 'jenkins-webhook'
webhook_configs:
- url: http://localhost:8888
因此,我们添加了一个新的子路由,匹配 alertname
:High_disk_space_usage
并使用 jenkins-webhook
接收器。
重新加载 Alertmanager 并开始执行器。假设 executor.sh
文件已放置在 /opt/prometheus/executor
路径下:
# cd /opt/prometheus/executor
# ./bin/prometheus-am-executor -l ':8888' ./executor.sh
2016/10/16 17:57:36 Listening on :8888 and running [./executor.sh]
我们已经启动了执行器(端口 8888
),并准备好接受来自 Alertmanager 的请求。
在触发任何测试警报之前,让我们回到 Jenkins 作业。你现在已经熟悉了它所期望的参数,以及通过 webhook
| executor
| jenkins
设置传递的参数,因此我们可以用以下 Shell 脚本替换占位符 Build 步骤的内容:
echo "alertname: ${alertname}"
echo "alertcount: ${alertcount}"
echo "instance: ${instance}"
export $(echo ${labels}|base64 -d)
NODE=$(echo ${instance}|cut -d: -f1)
LABEL_DIR=AMX_ALERT_${alertcount}_LABEL_path
APP_DIR='/opt/myapp/tmp'
if [ ${!LABEL_DIR} == ${APP_DIR} ];then
ssh ${NODE} "sudo rm -f ${APP_DIR}/*.tmp"
fi
为了测试这一切,我们需要 SSH 进入 Prometheus 正在监控的其中一个 ASG(Web 服务器)实例,并设置一个临时的假应用文件夹,如下所示:
# dd if=/dev/zero of=/tmp/dd.out bs=1M count=256
# mkfs.ext4 /tmp/dd.out
# mkdir -p /opt/myapp/tmp
# mount -oloop /tmp/dd.out /opt/myapp/tmp/
这应该会为我们提供一个小的文件系统来进行操作。接下来,我们填满它:
# dd if=/dev/zero of=/opt/myapp/tmp/dd.tmp bs=1M count=196
这已经超过了我们在 High_disk_space_usage
中设置的 20%,应该会触发警报。然后,执行器应该调用 Jenkins 并运行我们的作业:
我们可以看到 Jenkins 通过 SSH 连接到受影响的实例,然后清理我们的假应用 tmp
目录。
需要注意的是,尽管在本示例中我们允许使用 root 权限,但在其他情况下,你应该确保 Jenkins 能以非特权用户的身份处理给定的 tmp
目录,或者如果必须使用 sudo
,则应限制可用的命令和命令行参数。
总结
在这一章中,我们介绍了如何使用 Logstash 和 Elasticsearch 集中管理日志,并在 Kibana 中浏览这些日志。我们借助 Prometheus、Telegraf 和 Grafana 配置了指标收集和可视化。最后,我们通过 Prometheus 添加了监控并使用 Jenkins 实现自我修复。
下一章将带我们进入优化的领域。我们将讨论成本考虑和基于需求的扩展方法。
第八章 优化规模和成本
在优化方面,我们将从顶部开始,也就是说从设计阶段开始。
想象一遍又一遍地审视你的架构计划,直到你和你的同事们都确信,这是在当前信息条件下你能做到的最好的。现在想象一下,除非你有一个非常特殊的用例,否则其他人已经进行了类似的迭代,并慷慨地分享了结果。
回到现实中,幸运的是,我们并不远离。确实存在一个 AWS 的集体知识库,以博客文章、案例研究和白皮书的形式向任何开始他们的第一个云部署的人提供。
我们将从这些知识的精华样本开始,并将其应用于一个常见的架构示例中,试图在保持成本效益的同时实现最大的可伸缩性。
这个例子将是一个典型的前端(NGINX 节点)、后端(数据库集群)和 VPC 内的存储层部署:
尽管从技术上讲,我们整个部署都在互联网上,但上面的视觉分隔是为了强调 VPC 的网络隔离属性。
架构考虑
现在让我们逐个组件地检查这个部署,从 VPC 本身开始。
VPC
我假设如果你仍然持有这本书,你可能已经接受了 VPC 的方式。
CIDR
你预计会有多少个 VPC?它们会连接(VPC 对等连接)还是会在其他网络中进行桥接(VPN)?
这些问题的答案在选择 VPC 的 CIDR 时起到了作用。作为一个通则,建议避免常见的(家庭路由器)网络地址,如192.168.1.0
或10.0.0.0
。
如果你有多个 VPC,即使你没有立即需要将它们对等,也要跟踪和分配不同的 CIDR。
考虑一个 CIDR,它将允许足够大的子网以便在最小碎片化的情况下扩展实例(子网数量)。
子网和可用区
可用区(AZs)是我们如何为部署增加弹性的方式,因此我们应该至少有两个这样的区域。在某些配置中,例如需要集群仲裁的情况下,比如ZooKeeper,可能需要使用三个区域。在这种情况下,建议将仲裁成员放在不同的区域以更好地处理网络分区。为了适应这一点并保持费用低廉,我们可以在三个区域创建子网,在所有三个区域部署仲裁集群,并在其中的两个区域中部署其他组件(比如NGINX主机)。
让我们举一个例子,在这个例子中,我们在 VPC 中有一个 Zookeeper 和一个 Web 服务器(NGINX)组件。我们决定使用三个 AZ 并维护两组子网:公共和私有。前者通过 IGW 进行路由,后者通过 NAT:
在这里,我们有跨越所有三个可用区(AZs)和公共子网的 ELB。在私有子网空间中,我们有两个 Web 服务器和三个 ZooKeeper 节点的集群,这为我们提供了在最优成本下的良好弹性平衡。
VPC 限制
AWS 对每个账户强制实施某些初始限制,当你的环境开始扩展时,这些限制可能会让你感到意外。需要检查的重要限制有:实例、EBS 和 网络 限制,这些可以在 EC2 控制台中找到:
在请求扩展时,选择一个足够高的数字以提供扩展缓冲,但不要选择过高的数字,因为毕竟,限制是为了防止意外或错误的过度配置。
前端层
设置好子网后,我们可以开始考虑我们的 VPC 内的居民。
前端或应用层由我们的自动扩展组构成,第一个我们将面临的决定是选择 EC2 实例类型。
前端应用的配置将很大程度上决定选择内存优化型、计算优化型或存储优化型实例。借助来自同事(在内部应用的情况下)的帮助和合适的性能测试工具(或服务),你应该能够确定该应用程序主要使用哪种系统资源。
假设我们选择了C4 计算优化型实例类,这是 AWS 推荐用于 Web 服务器的实例。下一个问题是——选择什么大小?
一种猜测方法是:取我们希望能够支持的每秒请求的平均数量,部署我们能够承受的最小实例数量(为了弹性,至少选择两个实例),并选择该类中最小的实例大小,然后进行负载测试。理想情况下,两个节点的平均利用率应该保持在 50% 以下,以便应对流量激增和故障事件,其中剩余的主机会承担所有负载。如果结果远低于这个标准,那么我们应该寻找一个不同的类,选择更小的实例类型,以获得更好的性价比。否则,我们将继续增加 C4 实例的大小。
接下来是自动扩展的问题。我们已经选择了合适的类和实例大小,现在需要设置扩展阈值。首先,如果你幸运地拥有可预测的负载,那么使用定时操作,你的问题就可以解决了:
你可以简单地告诉 AWS 在 X 点钟时扩展我,再在 Y 点钟时缩减。其他人则需要设置警报和阈值。
我们已经决定,50%的平均利用率(假设是 CPU)是我们的上限,到那时我们应该已经开始扩展。否则,如果我们的两个节点中的一个发生故障,在这个速率下,另一个节点将不得不以最大容量工作。举个例子,CloudWatch告警可以设置为超过 40%的平均 CPU 使用率持续五分钟,触发自动扩展组的动作,将组的大小增加 50%(即一个实例)。
提示
为了防止不必要的扩展事件,调整冷却时间的值非常重要。它应该反映出新启动的实例完全投入操作并开始影响CloudWatch指标所需的预期时间。
为了更精细地控制自动扩展如何响应告警,我们可以使用步进扩展(参考:docs.aws.amazon.com/autoscaling/latest/userguide/as-scale-based-on-demand.html
)。步进调整根据阈值突破的严重程度,允许有不同的响应。例如,如果负载从 40%增加到 50%,则仅扩展一个实例;但如果负载从 40%跃升到 70%,则直接扩展到两个或更多实例。
提示
使用步进扩展时,冷却时间是通过实例预热选项设置的。
虽然我们希望快速扩展以防止任何服务中断,但缩减应该及时进行以节省按小时计费的费用,但又不能过早进行,以免导致扩展循环。
CloudWatch用于缩减的告警应持续的时间应远长于我们之前观察到的五分钟。而且,扩展和缩减的阈值之间的间隔应足够宽,以避免实例启动后很快被终止。
EC2 实例利用率只是触发器的一个例子;还可以考虑 ELB 指标,如总请求数、非 2XX 响应或响应延迟。如果选择使用其中任何一种,请确保您的缩减告警能够响应INSUFFICIENT_DATA状态,这种状态通常出现在没有流量的时期(例如,深夜)。
后端层
在应用程序背后,我们很可能会发现某种类型的数据库集群。以这个例子为例,我们选择了 RDS(MySQL/PostgreSQL)。然而,扩展和弹性设计可以轻松地转化为适用于 EC2 实例上的自定义数据库集群。
从高可用性的角度出发,对于 RDS 来说,相关功能称为多可用区(Multi-AZ)部署。这为我们提供了一个主 RDS 实例以及一个热备份备用副本作为故障转移解决方案。不幸的是,备用副本无法用于其他用途,也就是说,我们不能让它服务于只读查询等。
我们 VPC 中的多可用区设置看起来是这样的:
在PRIMARY发生故障时,RDS 会自动切换到STANDBY,并在此过程中更新相关的 DNS 记录。根据文档,典型的故障切换大约需要一到两分钟。
触发因素包括主节点不可用(因此 AWS 健康检查失败)、完整的 AZ 故障,或用户中断,如 RDS 实例重启。
到目前为止,使用 Multi-AZ,我们拥有一个相对弹性强的设置,但可能不是非常可扩展。在忙碌的环境中,通常会为写入操作分配一个主 DB 节点,而读取操作则从副本中进行。便宜的选择是向当前配置中添加一个副本:
在这里,我们将数据写入PRIMARY并从REPLICA读取,或者对于读密集型应用程序,读取可以同时从两者进行。
如果我们的预算允许,我们可以进一步提升,并在我们部署前端/应用节点的两个子网中提供一个REPLICA:
跨 AWS 区域的延迟已经非常低,但通过这种每区域的 RDS 分布,我们可以进一步降低延迟。所有主机都会写入PRIMARY,但是它们可以在读取时优先选择本地(同一区域)的REPLICA。
既然我们正在大手笔花费,额外的 RDS 性能提升可以通过预配置的 IOPS 来实现。如果你运行的是重负载并且需要高 RDS 存储 I/O,这一点值得考虑。
尽管是间接的,缓存也是一种非常有效的方式,通过减轻负载来提高 RDS 的可扩展性。
这里流行的软件选择包括Memcached和Redis。它们都可以轻松在本地设置(在每个应用主机上)。如果你希望利用共享缓存,可以在 EC2 上运行集群,或者使用 AWS 托管的 ElastiCache 服务。使用后者,我们可以再次拥有Multi-AZ配置,并且配备多个副本以提高弹性和降低延迟:
你会注意到,故障切换场景与 RDS 不同,因为没有备用实例。在PRIMARY发生故障时,ELASTICACHE会提升最新的REPLICA。
提示
注意,升级后,PRIMARY端点保持不变,但升级后的副本地址会发生变化。
对象存储层
为了实现轻松的可扩展性,我们必须在可能的情况下重点构建无状态应用程序。不在应用节点上保持状态意味着将任何有价值的数据存储在其他地方。一个经典的例子是WordPress,其中用户上传的文件通常保存在本地,这使得横向扩展这种设置变得困难。
虽然可以通过使用弹性文件系统(EFS)在 EC2 实例之间共享文件系统,但为了提高可靠性和可扩展性,我们最好使用像AWS S3这样的对象存储解决方案。
可以公平地说,访问 S3 对象并不像使用 EFS 卷那样简单,但 AWS 工具和 SDK 大大降低了门槛。为了便于实验,你可以从 S3 CLI 开始。最终,你会希望在应用程序中集成 S3 功能,可以使用以下其中之一:
-
Java/.NET/PHP/Python/Ruby 或其他 SDK(参考:
aws.amazon.com/tools/
) -
REST API (参考:
docs.aws.amazon.com/AmazonS3/latest/dev/RESTAPI.html
)
在之前的章节中,我们探讨了 IAM 角色作为一种方便的方式,将 S3 存储桶的访问权限授予 EC2 实例。我们还可以通过使用 VPC 端点来增强实例与 S3 之间的连接:
VPC 端点使你能够在 VPC 和其他 AWS 服务之间创建私有连接,无需通过 Internet、NAT 设备、VPN 连接或 AWS Direct Connect。端点是虚拟设备。它们是横向扩展、冗余且高度可用的 VPC 组件,允许你的 VPC 实例与 AWS 服务之间进行通信,同时不会对网络流量带来可用性风险或带宽限制。 | ||
---|---|---|
--docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-endpoints.html |
如果你的客户端位于不同的地理位置并且向你的存储桶上传内容,则可以使用 S3 传输加速(参考:docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html
)来提升他们的体验。只需在存储桶的设置页面点击启用即可:
我们现在已经覆盖了速度改进;可扩展性是 S3 服务本身内置的,对于成本优化,我们有不同的存储类别。
S3 目前支持四种类型(类别)的存储。最昂贵且最耐用的是标准类,也是默认存储类型。接下来是低频访问类(Standard_IA),价格较便宜,但需要注意的是,它确实是为较少访问的对象设计的,否则与之相关的检索成本会非常高。接下来是减少冗余类,尽管名字有些吓人,但它的耐用性依然相当高,达到了 99.99%。最后是冰川存储类,它类似于磁带备份,意味着对象会被归档,并且有 3-5 小时的检索时间(紧急检索可在 1-5 分钟内完成,但会有更高的费用)。
你可以在上传时指定对象的存储类(冰川存储类除外),或者通过 AWS 控制台、CLI 或 SDK 后续更改存储类。归档到冰川存储类是通过存储桶生命周期策略(存储桶设置页面)来完成的:
我们需要添加一条新规则,描述在什么条件下对象会被归档:
顺便提一下,生命周期规则也可以帮助您清理旧文件。
负载均衡层
在过去的疯狂时代,人们曾通过公共 IP 和 DNS 轮询来搭建 Web 服务器,但如今负载均衡器已经取而代之。
我们将研究 AWS ELB 服务,但这绝不是唯一的可选方案。实际上,如果您的使用场景对延迟非常敏感,或者您观察到频繁且短暂的流量激增,您可能希望考虑使用 NGINX 或 HAProxy 来部署自己的 EC2 负载均衡节点集群。
ELB 服务按每小时固定费用加带宽费用计费,因此我们可能无法通过减少费用来节省成本,但我们可以探索提高性能的方法。
跨区域负载均衡
在正常情况下,Classic ELB 会将其节点部署在我们的后台(应用程序)实例所在的区域,并根据这些区域转发流量。也就是说,区域A中的 ELB 节点将与同一区域的后台实例通信,区域B也适用相同的原则:
这样做是合理的,因为它可以确保最低的延迟,但有几点需要注意:
-
为了获得最佳负载分布,每个区域中应保持相等数量的后台节点。
-
客户端缓存 ELB 节点的 IP 地址将始终保持与相应后台实例的连接
为了改善这种情况,虽然会带来一些(最小的)延迟增加,我们可以在 Classic ELB 的属性中启用跨区域负载均衡:
这将改变流量分配策略,使得对特定 ELB 节点的请求将均匀分布到所有已注册(状态:InService)后台实例上,从而将我们之前的示意图更改为如下:
每个区域中后台节点数量不均将不再影响负载均衡,外部方也无法仅针对单个 ELB 实例进行攻击。
ELB 预热
ELB 服务的一个重要方面是它在一组特定类型的 EC2 实例上运行,这与我们的后台节点非常相似。考虑到这一点,ELB 会根据需求进行扩展,这一点与我们的自动扩展组类似,因此不会让人感到意外。
当传入流量波动在一定范围内时,这一方案效果很好,因为 ELB 可以吸收流量,或者流量逐渐增加,为 ELB 扩展和适应提供足够的时间。然而,剧烈的流量激增可能会导致 ELB 丢失连接,如果流量大到一定程度的话。
这种情况可以通过一种叫做预热的技术来防止,实际上就是在预期流量高峰前扩展 ELB。目前这不是用户端可以执行的操作,这意味着你需要联系 AWS 支持来提交 ELB 预热请求。
CDN 层
CloudFront或 AWS 的 CDN 解决方案是另一种提高 ELB 和 S3 服务性能的方法。如果你不熟悉 CDN 网络,一般来说,它们为不同地理位置的客户端提供更快的访问速度。此外,CDN 还会缓存数据,以便后续请求无需到达你的服务器(也叫源),从而大大减轻负载。
因此,假设我们的 VPC 部署在美国,如果我们在 ELB 和/或 S3 存储桶前设置CloudFront 分发,那么来自欧洲等地的客户端请求会被路由到最近的欧洲 CloudFront 节点,该节点会根据需要提供缓存响应或通过 AWS 内部高速网络从 ELB/S3 获取请求的数据。
要设置一个基本的Web 分发,我们可以使用CloudFront 仪表盘:
一旦我们开始使用,第二页会展示分发的属性:
方便的是,同一 AWS 账户中的资源会被建议。源是 CloudFront 需要连接的数据源,例如,位于我们应用程序前面的 ELB。在备用域名字段中,我们将输入我们的网站地址(比如www.example.org
),其余设置目前可以保留默认值。
当分发变为活动状态后,剩下的工作就是更新指向 ELB 的www.example.org
的 DNS 记录,将其改为指向分发地址。
Spot 实例
我们的最后一点是利用Spot实例进一步节省 EC2 成本。Spot 实例代表了 EC2 平台上未使用的资源,用户可以随时竞标。一旦用户成功竞标并分配到 EC2 实例,只要当前的 Spot 价格低于他们的出价,该实例就会持续存在,否则它会被终止(实例元数据中会提供通知,参考:docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html
)。
这些条件使得 Spot 实例非常适合于工作流,其中任务开始时间灵活,且如果实例终止,可以安全地恢复任务。例如,可以在 Spot 实例上运行短期的 Jenkins 作业(甚至有插件支持),或者用它来运行执行一系列小任务的工作流,这些任务会定期将状态保存到 S3/RDS。
AWS 计算器
最后,这是一款简单却实用的工具,可以帮助你了解计划部署的成本:calculator.s3.amazonaws.com/index.html
(记得取消勾选页面顶部的FREE USAGE TIER选项)。
如果你正试图比较本地部署与云计算的成本,那么以下内容可能对你有帮助:aws.amazon.com/tco-calculator/
。
总结
在这一章中,我们研究了优化 AWS 部署的可扩展性和运行成本的不同方法。
我们从基础的 VPC 及其核心属性开始,包括 CIDR、子网以及如何规划增长。我们讨论了提高前端、后端、存储和负载均衡组件性能的方法。接着,我们探讨了 AWS Spot 实例,作为执行低优先级批处理任务的一种非常高效的成本解决方案。
在下一章中,我们将进入安全领域,探讨如何更好地加强 AWS 环境的安全性。
第九章:保护您的 AWS 环境
安全性无疑是云计算——你应该做吗?辩论中的一个热门话题。
一方面,我们有我的硬件就是我的城堡这群人,他们觉得将计算环境委托给某个抽象实体——这个实体告诉你你随时拥有X台机器的计算能力,但你既看不见也摸不着,甚至连你的数据也无法掌控——是一件非常不自然的事情。
另一方面,我们有那些根本不介意云这一神秘概念的人。他们的主要兴趣在于以合理的成本即时获取几乎无限的计算资源。不幸的是,他们有时可能过于集中于快速完成工作,而忽视了前一群人提出的一些有效且健康的关注点。
然后是中间地带——那些意识到在迁移到云端时必须接受的牺牲,以及为弥补这些牺牲而采取的各种解决方案的人。也就是说,通过精心设计的应用程序和仔细规划的架构,您的环境可以保持足够的安全性,无论底层托管平台是什么类型。
我们将研究这些解决方案和实践中的一些,以尝试提高我们的 AWS 环境的安全性。
我们将涵盖:
-
使用 IAM 管理访问
-
VPC 安全性
-
EC2 安全性
-
安全审计
让我们开始吧。
使用 IAM 管理访问
AWS 身份和访问管理(IAM)是一个网络服务,帮助您安全地控制用户对 AWS 资源的访问。您使用 IAM 来控制谁可以使用您的 AWS 资源(身份验证),以及他们可以使用哪些资源和以何种方式使用(授权)。
参考:
docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html
我们将使用 IAM 来管理访问(无论是用户还是应用程序)到我们 AWS 账户下的服务。
保护根账户
当一个新的 AWS 账户被创建时,它会包含一个用户(账户所有者),也称为根登录。这个强大的用户拥有所有权限,包括终止 AWS 账户的选项。因此,通常建议仅将根登录用于高层账户管理,而日常操作则通过 IAM 用户账户进行。
我们将遵循这个建议,因此我们在注册 AWS 账户后做的第一件事就是以根用户登录,禁用任何不必要的身份验证机制,并创建一个权限较低的 IAM 用户账户。
让我们浏览到 AWS 控制台(参考:console.aws.amazon.com/console/home
):
注意到登录按钮下方的小字。这是我们需要点击的链接,用于访问根账户,它会带我们到一个稍微不同的登录页面,如下截图所示:
在这里,使用你的主要 Amazon 凭证;你应该能看到熟悉的控制台页面。点击右上角的名字:
选择安全凭证将带我们到根账户的安全选项:
启用多因素认证 (MFA);事实上没有理由不启用它。你可以购买硬件令牌设备,或者直接使用手机上的应用程序,如Google Authenticator。
删除访问密钥下的密钥。这些密钥用于 API 访问,而你在账户管理任务中很可能不需要它们。
接下来,点击左侧的账户设置链接,更新当前的密码策略。现如今有很多密码管理工具,选择一个复杂的密码并定期更改已不再是麻烦,所以尽情设置吧:
在同一页面上,我们可以禁用任何不打算使用的区域:
现在我们继续创建用于日常 AWS 使用的 IAM 账户。我们将用户组织成不同的组。首先,我们创建一个具有管理员权限的用户组,之后可以利用该组管理 AWS 账户的几乎所有方面。
在左侧选择用户组,创建一个新组并赋予其管理员权限。然后在用户下创建一个账户,并将其添加到该组。
在创建用户过程中,你会有机会创建 API 访问密钥(你也可以稍后创建),如果你计划使用 AWS CLI 或一般的程序访问,这些密钥会很有用。创建后,选择该用户并切换到安全凭证选项卡:
在这里,你可以选择创建访问密钥对,如果你之前没有创建的话,还可以设置用于 AWS 控制台的密码。如前所述,你应该借此机会启用MFA(进一步了解,请查看docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_configure-api-require.html
)。如果你计划通过 SSH 使用 CodeCommit 服务,这里是上传公钥的地方。
就是这样,从现在开始,你可以使用刚刚创建的 IAM 账户的用户名和密码登录 AWS 控制台,根账户则保留给特殊场合使用。
对于那些可能已经维护外部用户数据库的人,顺便提一下,有一些方法可以使用联邦身份认证将其集成。
注意
欲了解更多详细信息,请参考以下链接:aws.amazon.com/iam/details/manage-federation
docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers.html
VPC 安全性
如果您已经在 VPC 中部署了资源,那么您已经朝着正确的方向前进。在这里,我们主要关注网络安全以及 VPC 提供的增强安全性的工具或功能。
安全组
这些代表了我们在 AWS 文档中所说的第一层防护。安全组(SG)通常分配给 EC2 实例,提供一种有状态的防火墙,只支持允许规则。
它们非常灵活,一个 EC 实例可以有多个这样的组。规则可以基于主机 IP 地址、CIDR 或甚至其他安全组,例如,允许从组 ID sg-12345
进入的 HTTP:80
流量。
通常,在 VPC 内,我们会根据角色创建安全组,例如 web 服务器、数据库、缓存。同一组件的实例将被分配相应的安全组,从而调控平台不同组件之间的流量。
提示
常常会有冲动基于 VPC 的 CIDR 地址来允许流量,认为 VPC 是一个相对孤立的环境。尽量避免这种做法,并限制对真正需要访问的组件的权限。
数据库安全组应允许来自/到 web 服务器安全组的流量,但可能不允许来自缓存安全组的流量。
网络 ACL
第二层防护形式是网络 ACL。
ACL(访问控制列表)是无状态的,它们应用于实例所在的底层子网,规则的评估依据优先级,就像传统的防火墙一样。作为额外功能,您还可以设置拒绝策略。
提示
网络 ACL 位于 VPC 的边缘,因此在流量到达任何安全组之前会先进行评估。这个特性加上设置拒绝规则的能力,使得它们非常适合应对潜在的 DDOS 威胁。
总的来说,流量管理的两种方式在我们的 VPC 安全设计中各有其作用。ACL 应该存储一组较为广泛且不常变化的规则,并由灵活的安全组进行细粒度控制。
VPN 网关
如果您正在使用 VPC 作为本地基础设施的扩展,那么将这两者更紧密地连接起来会非常有意义。
您可以选择创建一个安全的 VPN 通道,而不是通过安全组或 ACL 限制外部访问,从而受益于隐式加密。
您可以通过硬件或软件 VPN 解决方案将 VPC 连接到您的办公室网络(参考:docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpn-connections.html
)。
对于更为苛刻的使用案例,可以通过 AWS Direct Connect 服务(参考: docs.aws.amazon.com/directconnect/latest/UserGuide/Welcome.html
)将 VPN 流量通过高速直接连接传输到 AWS。
VPC 对等连接
在类似的情况下,如果你不是使用办公网络,而是有另一个需要与主 VPC 通信的 VPC,你可以使用 VPC 对等连接:
VPC 对等连接是两个 VPC 之间的网络连接,允许你使用私有 IP 地址在它们之间路由流量。任一 VPC 中的实例都可以像在同一网络中一样相互通信。你可以在自己的 VPC 之间创建 VPC 对等连接,也可以在单一地区内与另一个 AWS 账户中的 VPC 创建对等连接。
AWS 使用现有的 VPC 基础设施来创建 VPC 对等连接;这既不是网关,也不是 VPN 连接,并且不依赖于独立的物理硬件。通信没有单点故障,也没有带宽瓶颈。
参考:
docs.aws.amazon.com/AmazonVPC/latest/PeeringGuide/vpc-peering-overview.html
你的 VPC 将能够直接通信(在同一地区内),因此你不需要暴露任何不需要显式暴露的服务。此外,你可以方便地继续使用私有地址进行通信。
EC2 安全性
更深入地探讨我们的 VPC,我们现在将关注增强 EC2 实例安全性的方式。
IAM 角色
IAM EC2 角色是推荐的方式来授予你的应用程序访问 AWS 服务的权限。
举个例子,假设我们在 Web 服务器 EC2 实例上运行一个 Web 应用,并且该应用需要能够将资源上传到 S3。
一种快速满足此要求的方式是创建一组 IAM 访问密钥,并将其硬编码到应用程序或其配置中。然而,这意味着从那时起,除非我们执行应用程序/配置部署,否则可能不容易更新这些密钥。此外,由于某些原因,我们可能会将同一组密钥与其他应用程序重复使用。
安全性隐患是显而易见的:重用密钥会增加暴露的风险,如果这些密钥被泄露,而硬编码密钥则大大增加了我们的响应时间(更难旋转这些密钥)。
上述方法的替代方案是使用角色。我们可以创建一个 EC2 角色,授予其对 S3 存储桶的写访问权限,并将其分配给 Web 服务器 EC2 实例。实例启动后,它将获得临时凭证,可以在其元数据中找到,并且这些凭证会定期更换。我们现在可以指示我们的 Web 应用从实例元数据中获取当前的凭证,并使用这些凭证执行 S3 操作。如果我们在该实例上使用 AWS CLI,我们会发现它默认会获取这些元数据凭证。
提示
角色只能在实例启动时与之关联,因此即使实例当前不需要,给所有主机分配角色也是一个好习惯。
角色可以用来扮演其他角色,使得实例能够通过扮演同一账户内或跨 AWS 账户的其他角色,暂时提升其权限(参考:docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
)。
SSH 访问
与 EC2 实例交互的最常见方式是通过 SSH。以下是一些建议,帮助你使 SSH 会话更加安全。
独立密钥
当启动一个普通的 EC2 实例时,它通常会关联一组 PEM 密钥,以便进行初始 SSH 访问。如果你还在团队中工作,我的建议是不要与同事共享相同的密钥对。
相反,一旦你或理想情况下是你的配置管理工具获得对实例的访问权限,应该为团队成员创建独立的用户账户并上传公钥(并在需要时授予 sudo
访问权限)。然后可以删除默认的 ec2-user
账户(在 Amazon Linux 上)和 PEM 密钥。
入口点
无论 EC2 实例的用途如何,通常情况下你不需要直接的外部 SSH 访问权限。
在 EC2 实例上分配公共 IP 地址并打开端口通常是出于便利性考虑,但这是一种不必要的暴露,也在某种程度上与最初使用 VPC 的理念相矛盾。
然而,SSH 无可否认是有用的。因此,为了平衡各种因素,可以设置一个带有公共地址的 SSH 网关主机。然后,你可以限制对该主机的访问,仅允许来自家庭和/或办公室网络的访问,并允许该主机通过 SSH 连接到 VPC 内的其他资源。
选择的节点成为 VPC 的管理入口点。
ELB 到处可见
延迟是一个重要因素。你可以在网上找到许多杰出的 AWS 用户撰写的工程文章,他们花费了大量时间和精力对 ELB 性能及其副作用进行了基准测试。
也许不令人意外的是,他们的研究发现,使用 ELB 相较于直接从后端 Web 服务器群组处理请求,确实会有一定的延迟惩罚。不过,另一方面,这样的额外层次,不论是 ELB 还是自定义 HAProxy 实例的集群,实际上在这些 Web 服务器前充当了一个保护屏障。
在 VPC 边缘使用负载均衡器,Web 服务器节点可以保持在私有子网中,如果你能够接受一定的延迟折衷,这无疑是一个不小的优势。
默认启用 HTTPS
服务如AWS 证书管理器使得使用 SSL/TLS 加密变得更加简便和经济。你将获得证书并且自动续订(在 AWS 内免费)。
是否应该加密 ELB 和 VPC 内的后端实例之间的流量是另一个好问题,但现在请务必为你的 ELB 添加证书,并在可能的地方强制使用 HTTPS。
加密存储
合理地说,由于我们关注的是加密 HTTP 流量,我们不应忽视静态数据。
AWS 上最常见的存储类型必须是 EBS 卷,其次是 S3。以上两项服务都支持强大且轻松实现的加密。
EBS 卷
首先需要注意,并非所有 EC2 实例类型都支持加密卷。在进一步操作之前,请参考此表:docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#EBSEncryption_supported_instances
另外,让我们来看一下哪些数据会被加密以及如何加密:
当你创建一个加密的 EBS 卷并将其附加到支持的实例类型时,以下类型的数据将被加密:
- 卷内的静态数据
- 在卷和实例之间传输的所有数据
- 从卷创建的所有快照
加密发生在托管 EC2 实例的服务器上,提供从 EC2 实例到 EBS 存储的数据传输加密。
参考:
docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html
请注意,数据在托管 EC2 实例的服务器上进行加密,也就是说是虚拟化管理程序进行加密。
自然地,如果你希望做得更彻底,你可以在实例本身管理加密。否则,你可以相对放心,因为每个卷都会使用一个单独的密钥进行加密,而这个密钥又是由与给定 AWS 账户关联的主密钥加密的。
在密钥管理方面,AWS 推荐你创建一个自定义密钥来替代默认为你生成的密钥。让我们创建一个密钥并开始使用它。
在 IAM 仪表盘中,选择左侧的加密密钥:
选择创建密钥并填写详细信息:
然后你可以定义谁可以管理密钥:
以及谁可以使用它:
结果应该在密钥列表中显示在仪表盘上:
现在,如果你切换到 EC2 控制台并选择创建一个新的 EBS 卷,那么自定义加密密钥应该作为一个选项可用:
现在你可以按照常规流程,将新的加密卷附加到 EC2 实例上。
S3 对象
S3 允许使用与 EBS 相同的AES-256算法对存储桶内的所有对象或部分对象进行加密。
有几种密钥管理方法可用(参考:docs.aws.amazon.com/AmazonS3/latest/dev/serv-side-encryption.html
):
-
你可以导入自己的外部密钥集。
-
你可以使用 KMS 服务在 AWS 中生成自定义密钥。
-
你可以使用 S3 服务的默认(唯一)密钥。
对现有数据进行加密可以在文件夹级别进行:
或者通过选择单个文件:
新数据可以根据需求进行加密,方法是在PUT
请求中指定一个头部(x-amz-server-side-encryption
),或者如果使用 AWS S3 CLI,则通过传递任意--sse
选项。
还可以通过使用存储桶策略(参考:docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html
)来拒绝任何未指定加密的上传尝试。
操作系统更新
如果你关注任何安全公告,你可能已经注意到新安全漏洞发布的频率。因此,说操作系统包在完全更新的 EC2 实例被配置好后,几天甚至几个小时就会变得过时,可能并不算夸张。除非最新的漏洞影响到 BASH 或 OpenSSL,否则我们往往会因大多数主机处于隔离环境(如 VPC)中而感到安心,从而一再推迟更新。
我相信我们都同意,这是一种令人恐惧的做法,可能存在的原因是更新生产系统时所带来的焦虑感。像自动扩展这样的服务也带来了合法的复杂性,但这可以转化为一种优势。让我们看看如何做到这一点。
我们将一个典型的 EC2 部署分为两组实例:静态(非自动扩展)和自动扩展。我们的任务是将最新的操作系统更新部署到这两组实例上。
在静态实例的情况下,由于某些应用程序特定的限制或其他类型的限制,无法进行扩展,我们将不得不采取众所周知的方法:首先在一个完全独立的环境中测试更新,然后更新我们的静态生产主机(通常一次一个)。
然而,通过 Auto Scaling,操作系统补丁更新可能会变得更加愉快。您可能记得在前几章中我们使用 Packer 和 Serverspec 生产和测试 AMI。类似的 Jenkins 管道也可以用于执行操作系统更新:
-
启动源 AMI。
-
执行包更新。
-
运行测试。
-
打包一个新的 AMI。
-
按阶段在生产环境中部署。
为了熟悉这个过程,我们当然需要付出相当的努力,确保测试、部署和回滚程序尽可能可靠,但最终的结果是值得付出这些努力的。
安全审计
AWS 提供了一些不错的工具来帮助您保持安全策略的有效性。这些工具会为您提供详细的审计报告,包括如何改进任何潜在风险区域的建议。此外,您还可以配置服务日志,从而更好地了解部署或整个 AWS 账户中发生的情况。
VPC 流日志
此服务允许您捕获通过 VPC 流动的网络流量信息。生成的日志(不幸的是,尚未完全实时)包含源/目的端口、源/目的地址、协议及其他相关细节(完整列表请见:docs.aws.amazon.com/AmazonVPC/latest/UserGuide/flow-logs.html#flow-log-records
)。除了生成一些很酷的图表来帮助识别网络瓶颈外,数据还可以用于发现异常行为。例如,您可以通过解析流日志并将任何可疑条目转发到监控解决方案来设计一个内部 IDS。
在 VPC 控制台中,选择一个 VPC 并切换到流日志选项卡:
点击创建流日志:您需要填写一些参数,如要使用的 IAM 角色(点击设置权限以创建一个)和所需的目标日志组名称。
几分钟后,所述日志组应出现在CloudWatch 仪表板的日志部分下:
在该组中,您将找到每个 EC2 实例(更准确地说是每个网络接口)对应的日志流,其中包含捕获的流量详细信息。
CloudTrail
CloudTrail 服务用于记录 AWS 账户内的 API 活动。这包括通过 AWS 控制台、CLI、SDK 或其他为您发出调用的服务所做的请求。此日志对安全审计和故障排除都很有帮助。收集的数据存储在 S3 中作为加密对象,并附带签名哈希,以帮助确保没有发生篡改。
要启用该服务,我们进入CloudTrail 仪表板,寻找开始使用或添加新日志按钮:
我们选择从所有区域收集数据,并将其存储在启用验证的新 S3 存储桶中。还可以在每次日志交付时接收通知,这对于进一步的处理任务非常有用。
返回仪表板后,我们点击新创建的轨迹以查看其设置:
我们启用加密,然后为新的 KMS 密钥输入名称。大约 15 分钟后,我们应该能在 API 活动历史仪表板选项卡下看到事件出现:
展开这些条目中的任何一项将提供更多信息,例如给定 API 调用使用的 access_key
和源 IP。
在 S3 存储桶中,我们会找到两个子文件夹:CloudTrail 存储 API 日志,CloudTrail-Digest 存储文件哈希值。
Trusted Advisor
Advisor 默认启用,并定期审查您的 AWS 账户,以识别任何风险或改进的领域。
它提供关于成本、性能、安全性和高可用性(HA)的洞察,如仪表板所示:
此时我们主要关注安全性提示:
按照我们在本章之前为确保根账户安全所采取的步骤,一切看起来都很正常。
除此视图外,还可以在 首选项 选项卡下配置每周电子邮件报告。
AWS Config
使用 Config 我们可以追踪、检查并对部署中发生的资源变更进行警报。
启用时,服务会对区域内找到的资源进行清单管理,并开始记录任何变化。
一旦检测到资源变更,例如添加了一个新规则到安全组,Config 会允许我们查看一个时间轴,显示当前和任何之前的资源变更详情。
另一个强大的功能是变更检查。在 Config 中,我们可以定义一组规则,用于评估每次资源变更,并在必要时生成警报。
让我们尝试这两种使用案例。
点击 开始使用,然后选择一个 存储桶名称 和 角色名称:
在下一页,我们可以选择一些规则来开始:
我们选择监控 CloudTrail、EBS 卷和 MFA 设置。完成设置后,返回到仪表板上的 规则 选项卡,我们可以添加更多规则。
注意
请注意,在撰写本文时,每个活动规则每月需要 $2 的费用。
点击 添加规则,并查找 restricted-ssh 规则,该规则会监控安全组中的 SSH 访问权限。设置新规则后,我们可以进行一些资源更改,看看 Config 如何对这些变更作出反应。例如,禁用 CloudTrail,并创建一个临时安全组,允许从任何地方进行 SSH 访问。
稍等片刻,我们可以看到 AWS Config 仪表板上的效果:
我们可以点击restricted-ssh条目查看更多细节。定位列表中的不合规条目并点击AWS Config时间轴图标:
我们可以看到该资源的两个记录状态。点击更改,我们可以看到发生了什么:
在这里,我们可以看到为什么我们的安全组资源被标记为不合规。
除了 AWS 提供的 Config 规则外,您还可以以 Lambda 函数的形式编写自己的规则(参考:docs.aws.amazon.com/config/latest/developerguide/evaluate-config_develop-rules.html
)。
自我渗透测试
在这里,我们讨论自我渗透测试作为一种廉价的替代方案,或者作为您在雇佣第三方进行正式测试之前的准备步骤(考虑到每次渗透测试通常是收费的)。
目标是建立一个可以按需和/或定期对我们的 VPC 部署进行内部和外部漏洞扫描的系统。
有两个社区项目可以帮助我们完成这项任务,它们是OpenVAS(参考:www.openvas.org
)和OpenSCAP(参考:www.open-scap.org
)。
设置这样一个自动扫描器的相对简单方法是使用一个预先制作的 AMI 和一些用户数据。本质上,您会在一个标准的 EC2 实例上安装前面提到的框架之一或两个,并从中创建一个 AMI。然后,启动这个 AMI 的新实例(可能是定期启动),并使用用户数据来启动扫描器,将要扫描的目标 URI 传递给它,然后通过电子邮件发送任何扫描报告或保存到 S3。
调度是通过 Auto Scale Group 实现的,它简单地启动一个节点,然后在 N 小时后终止它(即执行扫描所需的时间)。或者,您可以使用 CloudWatch 事件配合一些 Lambda 函数(参考:aws.amazon.com/premiumsupport/knowledge-center/start-stop-lambda-cloudwatch
)。
注意
请注意,漏洞扫描或类似活动需要首先获得 AWS 支持的批准(参考:aws.amazon.com/forms/penetration-testing-request
)。
遵循本章中的建议是创建一个更安全环境的一个步骤,但我们绝不能认为工作已经完成。有人说过,安全是一个过程,而不是产品,因此它或许应该是每天待办事项的一部分。
推荐您订阅相关的安全信息源或邮件列表。
AWS 维护了自己的一些信息源:
总结
在本章中,我们介绍了如何提高 AWS 账户整体安全性的一些方法。
我们查看了可以用于审计和对可疑活动发出警报的 AWS 服务,以及一些可以用于定期漏洞扫描的开源工具。
在下一章中,我们将查看一些流行的(以及较不常见的)AWS 技巧和窍门。
第十章:AWS 提示与技巧
在本章中,我想为你提供一些随机的建议。有些是我自己使用 AWS 的经验总结,其他一些则来自 AWS 白皮书或相关博客。
注意
关于此主题的几个链接:
d0.awsstatic.com/whitepapers/AWS_Cloud_Best_Practices.pdf
wblinks.com/notes/aws-tips-i-wish-id-known-before-i-started/
launchbylunch.com/posts/2014/Jan/29/aws-tips/
使用 VPC
除了最初的少量设置开销外,通常认为在 VPC 内部署基础设施会更好。AWS 默认提供了一个 VPC,并且通常会在其中部署资源,除非你另有要求。VPC 为你提供了更大的灵活性,能够更好地控制网络并增强安全性。此外,它是免费的。
将主路由表作为后备
如果你遵循前面的建议,你会发现一个新的 VPC 会附带一个标记为 Main 的路由表:
我建议保持当前设置,使用单一的本地路由,并为任何自定义路由需求创建额外的路由表。
这样,主路由表或默认路由表就成了一个安全网,用于任何被创建但未关联的子网,无论是出于失误还是故意。
保持在 VPC 内部
尽管诱人,但尽量避免暴露你的 VPC 资源,因为这样会违背目的。也就是说,不要给你的 EC2 实例分配公有 IP,虽然这可能会提供快捷的访问,而是使用一个指定的 ssh-gateway 主机(也叫做堡垒主机或跳跃主机)来进行跳转。
你只会为这台机器分配一个公有(弹性)IP,确保它的安全组只允许你家或工作地点的静态 IP 访问,并通过该 IP 连接(例如通过 ssh)到 VPC 中的其他实例。
提前创建 IAM 角色
我们已经讨论过 EC2 实例角色,这是为你的应用提供凭证的更好方式。
一个好的做法是,总是为你的实例创建并分配 IAM 角色,即使当时不需要且没有权限。
这是因为只有在启动 EC2 实例时才能分配 IAM 角色。
优先考虑组而非用户
当你创建第一个部署时,你可能不一定有太多用户需要访问你的 AWS 账户。
尽管如此,最好是将权限分配给 IAM 组,并让你的 IAM 用户成为该组的成员,而不是每次新增用户时都赋予其权限。
从长远来看,团队成员通常会需要(重复使用)相同的权限列表。
了解 AWS 服务限制
AWS 账户带有一些限制,这些限制可以在 AWS 控制台中找到:
这些措施旨在保护客户和服务提供商免受任何无意的使用。以下是一些示例:
-
您的 CloudFormation 模板中出现编码错误,导致意外分配大量存储或其他资源。
-
配置错误的自动扩展组,启动了数十个或数百个实例。
-
您的用户发起 API 调用,请求一个不寻常数量的实例。
如我们所见,前述的限制大多数情况下是一个很好的主意。
如果您处于生产环境中,准备迎接一个重大事件以及随之而来的流量激增,您当然希望了解您当前的 AWS 服务限制。大多数实例类型的初始限制为 20,VPC EIP 的限制为 5,存储类型的限制为 20 TB。
理想情况下,您应该在了解了预期使用基准(允许突发)之后,尽早审查这些内容,并在需要时联系 AWS 支持申请增加限制。
根据需要预热 ELB
在流量激增的问题上,虽然 ELB 性能卓越,但仍然可能会有需要预热它们的场合。
如您所知,ELB 是一组由 AWS 管理的 EC2 实例,运行专有的负载均衡软件。
一个算法确保 ELB EC2 实例的数量会根据您的应用流量模式而增长或缩减。这种自适应扩展过程基于一段时间内采集的平均流量数据,因此并不是非常迅速。
为确保此功能不会成为问题,AWS 允许您请求预热 ELB,也就是说,提前进行扩容。
如果您正在使用高级支持计划,您可能可以等到事件前几小时再处理;否则,您应该尽早联系支持团队,以便考虑到额外的响应时间。
您将被问及一系列与每秒请求次数、平均有效负载大小、事件持续时间以及其他流量特性相关的问题,这些将帮助 AWS 支持团队判断是否有必要进行预热。
使用终止保护
不言而喻,如果可以避免,应该避免在机器上保持状态。
毕竟,AWS 的美妙之处在于,它让您不再过于关注单个实例。它提倡集群或服务文化,关注终端节点的健康状态。
对于那些极少数的情况,我们必须拥有这些管理节点或类似的非自动扩展节点,不过,您从保护自己免于错误的 API 调用或控制台点击中,唯一能获得的就是好处。
尽可能进行标签标记
这听起来像是一项繁琐的工作,但它确实会带来后续的回报。无论是帮助你更清晰地理解 AWS 账单,还是在管理资源时提供更多的灵活性,标签总是非常有用的。
为你的工具配置标签功能,在资源创建时应用标签,然后定期扫描你的环境,查找任何没有标签的资源。
跨多个可用区部署
毫无疑问,在同一物理位置内部署应该会带来最低的延迟。
然而,在大多数使用场景下,增加的几毫秒延迟,换来的是多倍增加的可靠性,这是值得的。
尝试将你的部署至少跨越两个可用区。
增强你的 ELB 健康检查
默认的 ELB 健康检查允许你检查原始的 TCP 响应,或者深入到更高层的协议,检查 HTTP/200 响应。
两者都可以。一项基本的检查应该能帮助你入门,但随着应用程序及其依赖项的发展,你可能也需要增强健康检查。
假设你正在提供一个依赖缓存和数据库后端的 Web 应用程序。
如果 ELB 检查的是 TCP:80
,那么只要你的 HTTP 守护进程在运行,它就会收到一个 OK 响应。如果你检查的是 HTTP/200,反而是验证对应用程序文件(位于磁盘上的)的访问,但可能并没有更多的验证。
你也可以通过将 ELB 指向应用程序内部的专用健康检查端点,从中受益更多,该端点会在返回绿色信号之前验证所有依赖项(磁盘:OK,缓存:OK,数据库:OK)。但要小心不要影响整体应用性能:健康检查调用得越频繁,它应该越轻量。
将 SSL 负载卸载到 ELB 上
AWS 现在提供免费的 SSL 证书作为 证书管理服务 的一部分,该服务还会处理证书的续期。单单这个理由,就足够不错了。
在 ELB 上管理证书比在多个 EC2 后端实例上进行相同操作要方便得多。此外,通过将 SSL 操作委托给 ELB,至少可以节省一些 CPU 性能。
EIP 与公共 IP 的比较
关于这两种类型的几个要点,供你参考,万一你不太熟悉这些。
公共 IP:
-
你在启动实例时可以选择是否分配公共 IP。
-
地址会在重启后保持,但停止/启动时不会。
-
这些不收取额外费用
弹性 IP(Elastic IP):
-
你可以在实例启动后随时将 EIP 与实例关联或取消关联
-
EIP 会在重启或启动/停止操作之间保持关联
-
EIP 会产生费用(如果未使用时)
-
EIP 可以在 EC2 实例之间迁移
鉴于我们今天面临的 IPv4 地址短缺问题,AWS 正巧通过对任何闲置的 EIP 资源收费,巧妙地激励着合理的资源配置。
提示
做个绅士/淑女,使用完 IP 后释放它们。
留意按整点计费
AWS 允许您按使用量和按需付费,这是非常棒的。但是需要注意的是,AWS 按小时增量计费。
所以,假设您正在运行一些批处理作业,每 10 分钟启动和终止一个实例。一个小时零 10 分钟后,您将启动和终止六个实例(6x 最小增量为 1 小时),尽管它们每个实例的运行时间都不超过 10 分钟,但仍然会计算为 6 小时的可计费使用时间。
无论如何,为了避免意外情况,强烈建议您设置计费警报。这些是简单的 CloudWatch 警报,可以在估计账单达到阈值时通知您。
使用 Route53 ALIAS 记录
这是 Route53 服务特有的一种特殊内部 DNS 记录类型。
对于 AWS 用户,别名记录是 CNAME 的一个很好的替代选择(支持的资源)
一些主要优点包括:
-
别名直接解析为 IP 地址,节省了 CNAME 需要的额外查找
-
别名记录支持区域顶点,因此您可以创建一个使用域名顶部的别名(例如
mydomain.com
) -
别名记录允许高级 Route53 功能,如加权/延迟/地理路由和故障转移
-
别名查找没有与之相关的 AWS 成本
注意
注意:Route53 别名记录目前只能指向有限的一组 AWS 资源。有关更多信息,请参阅:docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-choosing-alias-non-alias.html
S3 桶命名空间是全局的
这意味着,如果在创建桶时遇到名称冲突,很可能是因为 AWS 宇宙中的其他人已经抢先了您的名称。
设计一个命名方案,提供一些唯一性;也许使用您组织的名称或桶名称的随机前缀/后缀。
S3 桶删除通常传播速度较慢。请注意您创建桶的区域。如果选择错误区域,根据我的经验,您需要删除然后等待 20-30 分钟,才能在正确的位置重新创建。
- 在 S3 桶名称中使用 - 而不是 .
看起来经常有一个问题,即是否应该将桶命名为 images-example-com
还是 images.example.com
。
有两件事需要考虑:
-
您是否希望使用 HTTPS 访问 S3?
-
您是否希望使用自定义域名代替默认的 S3 桶 URL?
严格来说,名称中带有点的桶在使用默认桶 URI 通过 HTTPS 访问时会显示 SSL 不匹配警告。
这是因为 S3 在 .amazonaws.com
域上运行,并且任何额外的点会使桶看起来像是子域(不包含在 SSL 证书中)。
另一方面,如果你想使用自定义域名(CNAME)指向你的存储桶,你必须使用点(.)。也就是说,存储桶名称必须与所说的自定义 URL 匹配,才能使 S3 的虚拟主机风格服务正常工作。
例如,我们将存储桶命名为 images.example.com
,并添加 DNS 记录 images.example.com
CNAME images.example.com.s3.amazonaws.com
。
然后,S3 会将传入的请求转发到任何与 HTTP 头中的主机名匹配的存储桶(参见:docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
)。
因此,似乎根据我们选择的名称,我们可以使用其中一个功能或另一个功能(HTTPS 与 CNAME)。但这个困境有解决方案:CloudFront。
在我们的存储桶前放置一个 CloudFront 分发允许指定自定义域名,并且可以使用自定义 SSL 证书。
随机化 S3 文件名
一个重要的事实是,S3 在分发数据时会考虑文件名(对象键)。当你的内容不使用顺序命名约定时,性能往往会更好。有关分发机制的更多详细信息,请参考:docs.aws.amazon.com/AmazonS3/latest/dev/request-rate-perf-considerations.html
初始化(预热)EBS 卷
过去,所有 EBS 存储都需要初始化,以避免首次访问时的性能损失,而当处理越来越大的卷时,这种性能损失会变得非常明显。如今,情况已经有所改善,因为新的卷不再需要预热(参考:docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-initialize.html
);然而,仍然需要考虑启动过程中的额外延迟(如果卷在启动时需要)与任何潜在的性能提升之间的权衡。
对于非常大的卷,初始化可能是不可行的,但在其他情况下,确实值得进行初始化。或者,如果你在 EC2 上运行自己的数据库服务器,那么无论卷的大小如何,你都应该考虑预热卷。
你可以使用建议的命令行步骤来测量执行此类优化所花费的时间(参考:docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-initialize.html
)。
总结
在本章中,我们探讨了一些技巧、窍门、事实和一般信息,这些内容在使用 AWS 时非常有用。
这自然只是这些公共秘密中的一小部分,如果你对 AWS 环境的独特性以及用户为应对这些特点而创造的各种技巧感到兴奋——我建议你查看:aws.amazon.com/blogs/aws/
。