Terraform 实战

978-7-115-58485-4
作者: 斯科特·温克勒(Scott Winkler)
译者: 赵利通
编辑: 谢晓芳

图书目录:

详情

本书基于实际项目,揭示如何使用Terraform自动扩展和管理基础架构。本书重点介绍了Terraform 0.12的语法、基础知识和高级设计(如零停机时间部署和创建Terraform提供程序)。本书主要内容包括如何使用Terraform,如何管理Terraform资源的生命周期,如何编程,如何在AWS云中部署多层的Web应用程序,如何实现无服务器的部署,如何通过Terraform部署服务器,如何实现零停机部署,如何测试、重构,如何扩展Terraform,如何通过Terraform自动部署,如何实现安全管理。 本书适合作为系统管理员、DevOps工程师、开发人员的自学和参考用书。

图书摘要

版权信息

书名:Terraform 实战

ISBN:978-7-115-58485-4

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


著    [美] 斯科特•温克勒(Scott Winkler)

译    赵利通

责任编辑 谢晓芳

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Original English language edition,entitled Terraform In Action by Scott Winkler published by Manning Publications, USA. Copyright ©2021 by Manning Publications.

Simplified Chinese-language edition copyright ©2022 by Posts & Telecom Press Co., LTD. All rights reserved.

本书中文简体字版由Manning Publications Co.授权人民邮电出版社有限公司独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。


本书基于实际项目,揭示如何使用Terraform自动扩展和管理基础架构。本书重点介绍了Terraform 0.12的语法、基础知识和高级设计(如零停机时间部署和创建Terraform提供程序)。本书主要内容包括如何使用Terraform,如何管理Terraform资源的生命周期,如何编程,如何在AWS云中部署多层的Web应用程序,如何实现无服务器的部署,如何通过Terraform部署服务器,如何实现零停机部署,如何测试、重构,如何扩展Terraform,如何通过Terraform自动部署,如何实现安全管理。

本书适合作为系统管理员、DevOps工程师、开发人员的自学和参考用书。


当Mitchell Hashimoto和我共同创立HashiCorp的时候,我们的目标是为新兴的云生态系统中的从业人员创建一套工具。置备基础设施是其中的关键一环,我们想创建出一个不同凡响的东西。我们在设计Terraform的时候,有3个目标。首先,无论使用什么平台,我们都想要有一个一致的、简单的工作流。其次,我们想要确保用户有高度的信心,不会有让用户感到意外的地方。最后,我们想让这个工具能够扩展,从而支持几乎所有的场景。

我第一次遇到Scott时,是在我们举办的HashiConf年度用户大会上。当时,他作为演讲人出席,讲到Ellie Mae在使用Terraform Enterprise,并且他们实现了一些模式和最佳实践,让Ellie Mae这家大型组织能够采用基础设施即代码的实践。Scott一直是Terraform生态系统中一位活跃的贡献者,为Minecraft贡献了一个新颖的提供程序,还贡献了一个Shell提供程序,以及公共注册表中的几十个模块。

当Scott联系我,说他要写一本关于Terraform的图书的时候,我感到异常兴奋,因为他拥有在小型项目到大型企业环境中使用Terraform和为Terraform“添砖加瓦”的丰富经验。本书为Terraform的新用户提供了循序渐进的入门介绍,但很快就深入剖析更加复杂的现实模式。第4章不仅针对模块提供了强有力的建议,指出模块应该提供封装和抽象,还介绍了关于文件和文件夹布局的最佳实践。

后续几章进一步展示了Terraform在IaaS上方的层中的应用,例如,无服务器平台和CI/CD管道。这使读者能够感受到Terraform的广泛适用性,以及如何对高层资源应用基础设施即代码。本书还讲解了一些高级模式,如使用蓝/绿部署和金丝雀部署模式实现零停机部署,它们对在有实时流量的情况下修改生产基础设施非常有帮助。除介绍如何使用Terraform之外,本书还讨论了如何使用模块、远程状态和Terraform Cloud在团队环境中进行协作。

对于有意为Terraform“添砖加瓦”的读者,本书还介绍了如何构建自定义提供程序。尽管大部分用户不太可能编写提供程序,但本书展示了编写提供程序多么简单,如果你需要支持自定义的内部系统或新的资源,本书将是很有用的参考。

Scott是Terraform专家,本书将他的几百甚至可能是几千小时的实践经验总结为简单易懂的实用建议。对于新用户及想要学习最佳实践的用户,本书是一本出色的入门指南,以及了解复杂模式的参考读物。无论是否熟悉Terraform,你都会发现在本书上投入的时间终会带来丰厚的回报。

——Armon Dadgar,HashiCorp联合创始人及CTO


当我开始撰写本书的时候,距离Terraform 0.12发布还有几个月的时间。我很幸运,成为了早期接触alpha版本的“幸运儿”之一,但它没有预想中那么有用。现有的提供程序都不兼容Terraform 0.12,所以我无法使用任何资源或数据源,我能做的只是试用输入变量、输出值和表达式。不过,我确实也从这个过程中得到了收获:最终,我得以开发一个简单的模板引擎,它后来成为本书第3章的基础。

在撰写本书时,我努力让自己与时俱进。但这种做法的问题是,你不知道前方会有什么东西打乱你的计划。好多次,由于新发布的某个特性破坏了现有功能,或者出现了新的设计模式,因此我不得不重写整章的内容。使用新技术让人很兴奋,但也会让人感到沮丧。

即使在如今,Terraform依然处在变化当中,但我感觉它终于稳定下来。Terraform已经成熟了很多,如今被全球数十万工程师用来管理价值数十亿美元的基础设施。Terraform仍然在变化,只是这种变化没有以前那么剧烈或者快速。根据Terraform 0.15中已经存在的功能判断,即使在即将发布的Terraform 1.0中,也不会大幅改变。

我很高兴自己写了这本书,因为Terraform的用户在不断增多,所以现在人们比以往任何时候都更需要有一本实用的入门指南,指导他们使用Terraform解决现实世界的问题。你可以找到许多关于Terraform的入门指南,但如果你想让自己的技能更上一层楼,该怎么办呢?本书就是为此撰写的。我希望本书能够启发你使用Terraform做一些了不起的事情。


许多人为本书的策划和出版费心费力。没有他们,本书就不会问世。首先,感谢策划编辑Katie Sposato Johnson,她为完善本书的结构和丰富本书的内容起到了重要作用。其次,我要感谢技术校对Niek Palm,他仔细测试了每行代码,并提供了宝贵的技术反馈。感谢Manning出版社的其他众多员工,以及为本书做出贡献的审阅者。

感谢我的同事Anthony Johnson向我介绍了Terraform,并帮助编写了本书的大纲。特别感谢HashiCorp的Armon Dadgar支持我撰写本书,并为本书作序。另外,还要感谢HashiCorp的企业营销副总裁Jay Fry推广本书,感谢HashiCorp的工程副总裁Paul Hinze针对本书讲解哪些主题提供建议。

感谢Manning的员工。感谢Brian Sawyer联系我撰写本书,感谢执行编辑Deirdre Hiam,感谢文字编辑Tiffany Taylor,感谢校对Jason Everett,感谢审稿编辑Ivan Martinović。感谢Manning所有帮助本书出版的人。

感谢所有的审阅者——Adam Kaczmarek、Alessandro Campeis、Amado Gramajo、Andrea Granata、Brian Norquist、Bruce Bergman、Dan Kacenjar、Emanuele Piccinelli、Enrico Mazzarella、Ernesto Cardenas Cangahuala、Geoff Clark、James Frohnhofer、Jürgen Hötzel、Kamesh Ganesan、Lakshmi Narasimhan、Leonardo Taccari、Luke Kupka、Matt Welke、Neil Croll、Paul Balogh、Riccardo Marotti、Sébastien Portebois、Stephen Goodman、Tim Bikalp和Vamsi Krishna。他们的建议让本书变得更好。

最后,感谢我的未婚妻Beatrice。她在我撰写本书的过程中一直支持我,并保证我手边总是有一杯热气腾腾的咖啡。感谢我的父母,他们让我接受教育并鼓励我写作,还要感谢一直对我有信心的祖父Jerry。


本书面向所有想要学习Terraform的人。可能你刚刚接触“基础设施即代码”,或者想要转变角色。也可能你已经有了多年经验,想要进一步提升自己的技能。无论你处于哪种状况,我相信本书都有能帮到你的地方。无论你是一名系统管理员、运营人员、网站可靠性工程师(Site Reliability Engineer,SRE)还是DevOps工程师,只要你想学习Terraform,选择本书就对了。

阅读本书不需要你有使用Terraform的经验。但是,我确实期望你有使用相关技术,特别是云的一些经验。你不必是一名解决方案架构师,但你应该知道云是什么,以及如何使用云。Terraform主要用于置备基于云的基础设施。

Terraform是一种表达能力很强的声明式编程语言。要想扩展Terraform,你需要具备一定的编程能力,最好具备使用Go语言编程的能力。你不必是一名程序员,但你知道的编程知识越多,学习体验就越好。

本书分为三部分。第一部分是一个训练营,帮助你尽快了解Terraform并使用Terraform。

第二部分探讨各种现实场景下Terraform的使用方式。

第三部分介绍Terraform中的高级场景,如测试、自动化和安全。

你应该按顺序阅读第1章到第7章。之后,你可以按任意顺序阅读。即使你不想继续阅读,我仍然建议你阅读第10章和第13章,因为这些主题对每个人都有用。

每章的代码都可在GitHub上获取(请在GitHub上搜索“terraform-in-action manning-code”)。这些代码是针对Terraform 0.15编写的,所以你需要安装Terraform 0.15(更新的版本应该也可以)。

本书包含许多源代码示例。源代码采用了等宽字体,以便与正文区分开。有时代码还会加粗显示,强调代码相比该章之前的步骤发生了变化,如添加了新特性。

在很多地方,原始源代码的格式有调整;我们添加了换行符,并改变了缩进,以便让代码适应本书的版面。极少数情况下,我们还会在代码清单中添加行延续标记(➥)。另外,在正文中描述代码的时候,还常常删除源代码中的注释。许多代码清单有注解,用于说明一些重要的概念。


Scott Winkler是一位DevOps工程师,还是著名的Terraform专家。他在HashiConf和HashiTalks上展示过自己的成果,并且是HashiCorp的核心贡献者。Scott在社区中很活跃,开发了许多模块和提供程序。在空闲时间,Scott喜欢骑马。Scott还负责提供Terraform的独立咨询服务。


本书的封面图片的原标题为“Habit d'un Morlakue de Sluin en Croatie”。此配图取自Grasset de Saint-Sauveur(1757—1810)于1797年出版于法国的一本名为Costumes de Différents Pays的收藏集,其中收录了多个国家和地区的服饰。每幅图都是精心绘制、手动上色的。Grasset de Saint-Sauveur的收藏集的丰富性提醒我们,仅仅200年前,世界上的不同地区还有着巨大的文化差异。由于彼此分隔,人们说着不同的方言和语言。在城市的街道上或者乡间,仅仅通过着装,我们就很容易知道人们居住在什么地方,以及他们的职业。

到了现在,我们的着装已经发生了改变,一度丰富的地区多样性也开始消亡。现在,我们已经很难区分来自不同大洲的居民,更不必说不同城镇、地区或国家的居民了。可能我们已经把文化多样性替换成为更加多样化的私人生活,但确定无疑的是已经替换为更加多样的、快节奏的技术生活。

如今,计算机图书有很多相似点,很难做出区分。Manning出版社根据Grasset de Saint-Sauveur的收藏集中的画作,将两个世纪前的丰富多样的地区生活融入封面设计,以颂扬计算机行业的创新性和推动力。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区投稿(直接访问www.epubit.com/contribute即可)。

如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。

异步社区

微信服务号


一部分的节奏先慢后快。你可以把本部分的几章内容视为学习Terraform的个人训练营。学习完第4章后,你将牢固掌握Terraform的基础知识,从而为学习后续几章中的高级主题做好准备。本部分将介绍如下内容。 第1章讲述Terraform的基础知识,如为什么创建Terraform、它解决什么问题,以及它与其他类似技术相比有哪些优缺点。该章最后给出一个在AWS上部署EC2实例的简单示例。 第2章深入介绍Terraform,讲解资源的生命周期和状态管理。该章不仅探讨Terraform如何生成及应用执行计划,以便对管理的资源执行CRUD操作,还介绍状态在此过程中承担什么角色。 第3章介绍变量和函数。虽然Terraform是一种声明式编程语言,这让它的表达能力受到限制,但使用for表达式和局部值,依然能够进行一些有趣的操作。 第4章主要演示一个将前面的知识点汇总起来的项目。我们将使用Terraform部署一个完整的Web服务器和数据库,看一下如何使用嵌套模块来配置Terraform。


本章要点:

Terraform是一种部署技术,任何想要通过基础设施即代码(Infrastructure as Code,IaC)方法来置备和管理基础设施的人,都可以使用这种技术。基础设施指的主要是基于云的基础设施,不过从技术上讲,任何能够通过应用程序编程接口(Application Programming Interface,API)进行控制的东西都可以算作基础设施。基础设施即代码是通过机器可读的定义文件来管理和置备基础设施的过程的。我们使用IaC来自动完成原本要由人手动完成的过程。

所谓置备,指的是基础设施部署,而不是配置管理,后者主要处理应用程序交付,特别是在虚拟机(Virtual Machine,VM)上交付。Ansible、Puppet、SaltStack和Chef等配置管理(Configuration Management,CM)工具已经存在多年,非常流行。Terraform并没有取代这些工具,至少不会完全取代,因为基础设施置备和配置管理在本质上是不同的问题。即使如此,Terraform也会提供原本只有CM工具会提供的一些功能,许多公司在采用了Terraform之后,发现自己并不需要CM工具。

Terraform的基本原则是,它允许编写人类可读的配置代码来定义IaC。借助配置代码,你可以把可重复的、短暂的、一致的环境部署到公有云、私有云和混合云上的供应商(参见图1.1)。

本章会先介绍Terraform相对于其他IaC技术的优缺点,以及它如何从这些技术中脱颖而出,然后通过把一个服务器部署到AWS,并使用Terraform的一些动态特性来改进它,演示Terraform的“Hello World!”示例。

图1.1 Terraform可以把基础设施部署到任何云或者混合云中

近来有大量关于Terraform的宣传,但这种宣传有理有据吗?Terraform并不是唯一的IaC技术,还有其他许多工具也能完成同样的工作。软件部署是利润颇丰的市场领域,Terraform为什么能够在这个领域与Amazon、Microsoft和Google等公司的技术竞争呢?有6个关键特征让Terraform与众不同,给它带来了竞争优势。

表1.1将Terraform与其他IaC工具进行了对比。

表1.1 Terraform与其他IaC工具的对比

名称 关键特征
置备工具 易于使用 免费、开源 声明式 云无关 表达能力强、可扩展
Ansible × × × ×
Chef × × × ×
Puppet × × × ×
SaltStack × × × × ×
Terraform × × × × × ×
Pulumi × × × ×
AWS CloudFormation × × ×
GCP Deployment Manager × × ×
Azure Resource Manager ×

技术对比

从技术上讲,Pulumi最接近Terraform,唯一的区别在于它不是声明式的。Pulumi团队认为这是Pulumi相较于Terraform的优势,但Terraform也有一个云开发工具包(Cloud Development Kit,CDK),允许实现相同的功能。

Terraform的设计受到了AWS CloudFormation的启发,并且与GCP Deployment Manager和Azure Resource Manager有很相近的地方。那些技术虽然也不错,但都不是与具体云无关的技术,也都不是开源的。它们只能用于特定的云供应商,并且一般不如Terraform简洁和灵活。

Ansible、Chef、Puppet和SaltStack都是配置管理工具,而不是基础设施置备工具。它们解决的问题类别与Terraform有些区别,不过也存在重叠的地方。

1.1.1 置备工具

Terraform是一种基础设施置备工具,而不是配置管理工具。置备工具部署和管理基础设施,而配置管理工具(如Ansible、Puppet、SaltStack和Chef)将软件部署到现有服务器上。一些配置管理工具也能够执行一定程度的基础设施置备,但不如Terraform,因为它们并不是为这类任务设计的。

配置管理工具和置备工具之间的区别主要在于理念。配置管理工具常用于管理可变基础设施,而Terraform和其他置备工具常用于管理不可变基础设施。

可变基础设施意味着在现有服务器上执行软件更新。不可变基础设施则不关心现有服务器,它把基础设施视为用后即可丢弃的商品。这两种范式之间的区别可归结为复用思想与用后丢弃思想的区别。

1.1.2 易于使用

即使是非程序员,也可以快速、轻松地学会Terraform的基础知识。到第4章结束时,你将具备中级Terraform用户必备的技能。细想一下,这简直让人难以置信。当然,要精通Terraform就是另外一回事了,不过对于大部分技能都是如此。

Terraform之所以如此易用,主要原因在于其代码是用一种称作HashiCorp Configuration Language(HCL)的领域特定的配置语言编写的。HashiCorp开发了这种语言,用来替代更加冗长的JSON和XML等配置语言。HCL试图在人类可读性和机器可读性之间达到一种平衡,并受到了这个领域中一些早期尝试(如libucl和Nginx配置)的影响。HCL与JSON完全兼容,这意味着HCL能够完全转换为JSON,反之亦然。这就使得与Terraform之外的系统进行互操作或者动态生成配置代码变得十分简单。

1.1.3 免费且开源的软件

Terraform的引擎称作Terraform core,这是一款免费且开源的软件,通过Mozilla Public License v2.0提供。该许可规定,任何人都可以出于个人目的和商业目的使用、分发或修改软件。免费这一点很好,因为这意味着你在使用Terraform时不必担心会承担额外的费用。另外,它使得产品及其工作方式对用户来说变得透明。

Terraform没有提供高级版本,但提供了商业解决方案和企业解决方案(Terraform Cloud和Terraform Enterprise),可成规模运行Terraform。第6章将介绍这些解决方案,在第12章中,我们将自己实现一个Terraform Enterprise。

1.1.4 声明式编程

声明式编程指的是表达计算逻辑(做什么),但不描述控制流(怎么做)。你不必编写一步步执行的指令,只要描述自己想要的结果即可。数据库查询语言(SQL)、函数式编程语言(Haskell、Clojure)、配置语言(XML、JSON)和大部分IaC工具(Ansible、Chef、Puppet)都是声明式编程语言的示例。

声明式编程语言是与命令式(或过程式)编程相对的。命令式语言使用条件分支、循环和表达式来控制系统流程、保存状态和执行命令。几乎所有传统编程语言(如Python、Java、C等)都是命令式编程语言。

注意 声明式编程关注的是结果,而不是过程。命令式编程关注的是过程,而不是结果。

1.1.5 云无关

云无关指的是能够使用一组相同的工具和工作流,无缝运行在任意云平台上。Terraform是云无关的,使用Terraform把基础设施部署到AWS与部署到GCP、Azure甚至私有数据中心一样简单(参见图1.2)。云无关很重要,因为这意味着你不会被局限于特定的云供应商,也不需要在每次改变云供应商时学习一种全新的技术。

图1.2 使用Terraform同时部署到多个云

Terraform通过提供程序(provider)与不同的云集成。提供程序是Terraform插件,用于与外部API进行交互。每个云供应商都会维护自己的Terraform提供程序,使Terraform能够管理该云中的资源。提供程序是使用Go语言编写的,并作为二进制文件分发到Terraform注册表上。它们负责进行身份验证、发出API请求以及处理超时和错误。在这个注册表中,有数百个已经发布的提供程序,它们协同起来,使你能够管理数千种不同的资源。第11章将会对此进行介绍,你甚至可以编写自己的Terraform提供程序。

1.1.6 表达能力强且高度可扩展

与其他声明式IaC工具相比,Terraform的表达能力强,且高度可扩展。通过使用条件语句、for表达式、指令、模板文件、动态块、变量和许多内置函数,我们可以轻松地编写代码来实现自己的目的。表1.2从技术的角度对比了Terraform和AWS CloudFormation(催生Terraform的技术)。

表1.2 Terraform和AWS CloudFormation的技术对比

名称 语言特性 其他特性
本身提供的函数 条件语句 for循环 类型 支持插件 模块化 等待条件
Terraform 115个 字符串、数字、列表、映射、布尔值、对象、复杂类型
AWS
CloudFormation
11个 字符串、数字、列表 有限程度

1.2 “Hello Terraform!”

本节介绍Terraform的一种经典用例——在AWS上部署一个虚拟机(EC2实例)。我们将使用Terraform的AWS提供程序来代表我们发出API调用和部署EC2实例。完成部署后,我们将让Terraform销毁该实例,避免服务器一直运行,造成越来越多的费用。图1.3显示了该操作的部署流程。

这个场景有一个先决条件——你必须安装了Terraform 0.15.X,并具有AWS的访问凭据。部署项目的步骤如下所示。

(1)编写Terraform配置文件。

(2)配置AWS提供程序。

(3)使用terraform init初始化Terraform。

(4)使用terraform apply部署EC2实例。

(5)使用terraform destroy进行清理。

图1.4演示了“Hello Terraform!”部署的工作流程。

图1.3 使用Terraform在AWS上部署一个EC2实例的架构

图1.4 “Hello Terraform!”的部署流程

1.2.1 编写Terraform配置

Terraform通过读取配置文件来部署基础设施。要告诉Terraform部署一个EC2实例,需要使用代码来声明该EC2实例。为此,先要创建一个新文件,将其命名为main.tf,并添加代码清单1.1中的内容。.tf扩展名表示这是一个Terraform配置文件。Terraform在运行时,将读取工作目录中所有具有.tf扩展名的文件,并把它们连接起来。

注意 本书中的所有代码均可在GitHub上通过搜索“terraform-in-action/manning-code”获取。

代码清单1.1 main.tf的内容

resource "aws_instance" "helloworld" {    ⇽---  声明一个名为“HelloWorld”的aws_instance资源
  ami           = "ami-09dd2e08d601bff67"  ⇽--- EC2实例的特性 
  instance_type = "t2.micro"
  tags = {
    Name = "HelloWorld"
  }
}

注意 此Amazon机器映像(Amazon Machine Image,AMI)仅对us-west-2地区有效。

代码清单1.1中的代码声明,我们希望Terraform置备一个t2.micro AWS EC2实例,使其具有Ubuntu AMI和一个名称标签。对比下面给出的等效的CloudFormation代码,可以看到Terraform代码要清晰得多,也简洁得多。

{
    "Resources": {
        "Example": {
            "Type": "AWS::EC2::Instance",
            "Properties": {
                "ImageId": "ami-09dd2e08d601bff67",
                "InstanceType": "t2.micro",
                "Tags": [
                    {
                        "Key": "Name",
                        "Value": "HelloWorld"
                    }
                ]
            }
        }
    }
}

这个EC2代码块是Terraform资源的一个示例。在Terraform中,资源是最重要的元素,因为它们置备虚拟机、负载均衡器、NAT网关等基础设施。资源被声明为HCL对象,具有resource类型和两个标签。第一个标签指定了要创建的资源的类型,第二个标签是资源的名称。名称并没有特别的意义,只用来在给定模块作用域内引用该资源。类型与名称合起来构成资源标识符,每个资源的标识符都是唯一的。图1.5显示了Terraform资源块的语法。

图1.5 资源块的语法

每个资源都有输入和输出。输入称作实参,输出称作特性。实参通过资源进行传递,也可作为资源特性使用。另外,资源还有计算特性,但只有在创建了资源后才能使用它们。计算特性包含计算得到的关于管理资源的信息。图1.6显示了aws_instance资源的实参、特性和计算特性的示例。

图1.6 aws_instance资源的实参、特性和计算特性的示例

1.2.2 配置AWS提供程序

接下来,我们需要配置AWS提供程序。AWS提供程序负责理解API交互、发出经过身份验证的请求,以及为Terraform提供资源。下面通过添加一个provider块来配置AWS提供程序。按照代码清单1.2更新main.tf中的代码。

代码清单1.2 main.tf

provider "aws" {    ⇽---  声明AWS提供程序
![箭头08{5%}](/api/storage/getbykey/original?key=22031b3595d646878b29) region = "us-west-2"    ⇽---  配置部署地区
}

resource "aws_instance" "helloworld" {
  ami           = "ami-09dd2e08d601bff67"
  instance_type = "t2.micro"
  tags = {
    Name = "HelloWorld"
  }
}

注意 在置备基础设施之前,需要先获得AWS凭据。凭据可以存储到凭据文件中或者环境变量中。

与资源不同,提供程序只有一个标签Name。这是该提供程序在Terraform注册表中发布时使用的正式名称(如“aws”代表AWS,“google”代表GCP,“azurerm”代表Azure)。提供程序块的语法如图1.7所示。

图1.7 提供程序块的语法

注意 Terraform注册表是一个全球商店,用来分享版本化提供程序的二进制文件。当Terraform初始化时,会从该注册表自动查找和下载任何必要的提供程序。

提供程序没有输出,只有输入。通过传递输入(或配置实参)给provider块,可以配置提供程序。配置实参包括服务端点URL、地区、提供程序版本、通过API身份验证所需的任何凭据等。图1.8演示了其注入过程。

图1.8 当发出API调用时,配置的提供程序如何把凭据注入aws_instance中

通常,你不会想要把凭据信息作为纯文本传递给提供程序,特别是以后要把这些代码签入版本控制系统的时候更是如此。因此,许多提供程序允许从环境变量或者共享凭据文件中读取凭据。如果对凭据管理感兴趣,建议阅读第13章,详细了解这个主题。

1.2.3 初始化Terraform

在让Terraform部署EC2实例之前,我们首先必须初始化工作空间。尽管我们已经声明了AWS提供程序,但是Terraform仍然需要从Terraform注册表下载和安装二进制文件。至少需要为所有工作空间执行一次初始化。

运行terraform init命令可以初始化Terraform。运行该命令将看到如下输出。

$ terraform init

Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v3.28.0...    ⇽---  Terraform获取AWS提供程序的最新版本
- Installed hashicorp/aws v3.28.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the
provider selections it made above. Include this file in your version
control repository so that Terraform can guarantee to make the same
selections by default when you run "terraform init" in the future.

_Terraform has been successfully initialized!    ⇽---  我们真正关心的只有这条信息
__
You may now begin working with Terraform. Try running "terraform plan" to
see any changes that are required for your infrastructure. All Terraform
commands should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget,
other commands will detect it and remind you to do so if necessary. 

注意 如果还没有安装Terraform,需要先进行安装,然后才能运行此命令。

1.2.4 部署EC2实例

现在,我们就准备好使用Terraform部署EC2实例了。这需要执行下面的terraform apply命令。

警告 完成此操作后会启用EC2和CloudWatch Logs,这可能会导致对你的AWS账户收费。

$ terraform apply

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

Terraform will perform the following actions:

 # aws_instance.helloworld will be created
  + resource "aws_instance" "helloworld" { 
      + ami                                = "ami-09dd2e08d601bff67"    ⇽---  ami特性
      + arn                                = (known after apply) 
      + associate_public_ip_address        = (known after apply)
      + availability_zone                 = (known after apply) 
      + cpu_core_count                    = (known after apply)
      + cpu_threads_per_core               = (known after apply) 
      + get_password_data                  = false
      + host_id                           = (known after apply) 
*      + id                                 = (known after apply)
      + instance_state                    = (known after apply) 
*      + instance_type                      = "t2.micro"    ⇽---  instance_type特性
      + ipv6_address_count                 = (known after apply) 
      + ipv6_addresses                    = (known after apply)
      + key_name                           = (known after apply) 
      + network_interface_id               = (known after apply)
      + outpost_arn                        = (known after apply) 
      + password_data                      = (known after apply)
      + placement_group                    = (known after apply) 
      + primary_network_interface_id     = (known after apply)
      + private_dns                      = (known after apply) 
      + private_ip                       = (known after apply)
      + public_dns                       = (known after apply) 
      + public_ip                        = (known after apply)
      + security_groups                  = (known after apply) 
      + source_dest_check                 = true
      + subnet_id                        = (known after apply) 
      + tags                             = {    ⇽---  Tags特性
          +  "Name"                      = "HelloWorld"
        }
      + tenancy                          = (known after apply) 
      + volume_tags                      = (known after apply)
      + vpc_security_group_ids           = (known after apply) 

      + ebs_block_device {
          + delete_on_termination        = (known after apply) 
          + device_name                  = (known after apply)
          + encrypted                    = (known after apply) 
          + iops                         = (known after apply)
          + kms_key_id                   = (known after apply) 
          + snapshot_id                  = (known after apply)
          + volume_id                    = (known after apply) 
          + volume_size                  = (known after apply)
          + volume_type                  = (known after apply)
        }

      + ephemeral_block_device { 
          + device_name                  = (known after apply)
          + no_device                    = (known after apply) 
          + virtual_name                 = (known after apply)
        }

      + metadata_options { 
          + http_endpoint                  = (known after apply)
          + http_put_response_hop_limit    = (known after apply) 
          + http_tokens                   = (known after apply)
        }

      + network_interface { 
          + delete_on_termination        = (known after apply)
          + device_index                  = (known after apply) 
          + network_interface_id          = (known after apply)
        }

      + root_block_device { 
          + delete_on_termination          = (known after apply)
          + device_name                    = (known after apply) 
          + encrypted                     = (known after apply)
          + iops                          = (known after apply) 
          + kms_key_id                    = (known after apply)
          + volume_id                     = (known after apply) 
          + volume_size                   = (known after apply)
          + volume_type                   = (known after apply)
        }
  }
*Plan: 1 to add, 0 to change, 0 to destroy.    ⇽---  操作的摘要

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

Enter a value:     ⇽---  手动批准步骤

提示 如果收到错误“No Valid Credentials Sources Found”,说明Terraform无法通过AWS的身份验证。

CLI输出称为执行计划,描述了Terraform计划执行哪些操作来得到人们期望的状态。在继续操作前,作为一种健全性检查,检查执行计划是一个好主意。除非在拼写时出错,否则这里不应有什么奇怪的地方。检查完执行计划后,通过在命令行输入yes批准执行。

一两分钟后(置备EC2实例大概需要这么长时间),apply即成功完成。下面是一些示例输出。

aws_instance.helloworld: Creating...
aws_instance.helloworld: Still creating... [10s elapsed]
aws_instance.helloworld: Still creating... [20s elapsed]
aws_instance.helloworld: Creation complete after 25s [id=i-070098fcf77d93c54]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed. 

要验证资源已被创建,你可以在AWS的EC2控制台找到它,如图1.9所示。注意,此实例位于us-west-2地区,因为我们在提供程序中就是这么设置的。

图1.9 AWS控制台中的EC2实例

资源的状态信息存储在一个名为terraform.tfstate的文件中。不要被扩展名.tfstate误导,它其实就是一个JSON文件。使用terraform show命令可以从状态文件输出人类可读的输出,这使得列举Terraform管理的资源的信息非常方便。下面是一条terraform show命令的执行结果。

$ terraform show
# aws_instance.helloworld:
resource "aws_instance" "helloworld" {
    ami                                = "ami-09dd2e08d601bff67"
    arn                                =
    ➥"arn:aws:ec2:us-west-2:215974853022:instance/i-070098fcf77d93c54"
    associate_public_ip_address        = true
    availability_zone                  = "us-west-2a"
    cpu_core_count                     = 1
    cpu_threads_per_core               = 1
    disable_api_termination            = false
    ebs_optimized                      = false
    get_password_data                  = false
    hibernation                        = false
   id                                 = "i-070098fcf77d93c54"    ⇽---  id是一个重要的计算特性
    instance_state                     = "running"
    instance_type                      = "t2.micro"
    ipv6_address_count                 = 0
    ipv6_addresses                     = []
    monitoring                         = false
    primary_network_interface_id     = "eni-031d47704eb23eaf0"
    private_dns                      =
    ➥"ip-172-31-25-172.us-west-2.compute.internal"
    private_ip                       = "172.31.25.172"
    public_dns                       =
    ➥"ec2-52-24-28-182.us-west-2.compute.amazonaws.com"
    public_ip                        = "52.24.28.182"
    secondary_private_ips            = []
    security_groups                  = [
        "default",
    ]
    source_dest_check                = true
    subnet_id                        = "subnet-0d78ac285558cff78"
    tags                             = {
        "Name"                       = "HelloWorld"
    }
    tenancy                          = "default"
    vpc_security_group_ids           = [
        "sg-0d8222ef7623a02a5",
    ]

    credit_specification {
        cpu_credits = "standard"
    }

    enclave_options {
        enabled = false
    }

    metadata_options {
        http_endpoint                  = "enabled"
        http_put_response_hop_limit    = 1
        http_tokens                    = "optional"
    }

    root_block_device {
        delete_on_termination        = true
        device_name                  = "/dev/sda1"
        encrypted                    = false
        iops                         = 100
        tags                         = {}
        throughput                   = 0
        volume_id                    = "vol-06b149cdd5722d6bc"
        volume_size                  = 8
        volume_type                  = "gp2"
    }
}

这里的特性远多于我们一开始在资源块中设置的特性,这是因为aws_instance中的特性大部分是可选特性或计算特性。通过设置可选实参,你可以自定义aws_instance。如果你想知道有哪些可选实参,可以参阅AWS的提供程序文档。

1.2.5 销毁EC2实例

现在是时候跟EC2实例说再见了。当不再使用基础设施时,应该销毁它们,因为在云中运行基础设施是要收费的。Terraform提供了一个特殊命令——terraform destroy,用于销毁全部资源。当运行此命令时,Terraform将会给出提示,要求你手动确认销毁操作。

$ terraform destroy
aws_instance.helloworld: Refreshing state... [id=i-070098fcf77d93c54]

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
 - destroy

Terraform will perform the following actions:

  # aws_instance.helloworld will be destroyed
  - resource "aws_instance" "helloworld" {
      - ami                              = "ami-09dd2e08d601bff67" -> null
      - arn                              = "arn:aws:ec2:us-west-2:215974853022:
        ➥instance/i-070098fcf77d93c54" -> null
      - associate_public_ip_address      = true -> null
      - availability_zone                = "us-west-2a" -> null
      - cpu_core_count                   = 1 -> null
      - cpu_threads_per_core             = 1 -> null
      - disable_api_termination          = false -> null
      - ebs_optimized                    = false -> null
      - get_password_data                = false -> null
      - hibernation                      = false -> null
      - id                               = "i-070098fcf77d93c54" -> null
      - instance_state                   = "running" -> null
      - instance_type                    = "t2.micro" -> null
      - ipv6_address_count               = 0 -> null
      - ipv6_addresses                   = [] -> null
      - monitoring                       = false -> null
      - primary_network_interface_id     = "eni-031d47704eb23eaf0" -> null
      - private_dns                      =
        ➥"ip-172-31-25-172.us-west-2.compute.internal" -> null
      - private_ip                       = "172.31.25.172" -> null
      - public_dns                       =
        ➥"ec2-52-24-28-182.us-west-2.compute.amazonaws.com" -> null
      - public_ip                        = "52.24.28.182" -> null
      - secondary_private_ips            = [] -> null
      - security_groups                  = [
          - "default",
        ] -> null
      - source_dest_check                = true -> null
      - subnet_id                        = "subnet-0d78ac285558cff78" -> null
      - tags = {
          - "Name"                       = "HelloWorld"
        } -> null
      - tenancy                          = "default" -> null
      - vpc_security_group_ids           = [
          - "sg-0d8222ef7623a02a5",
        ] -> null

      - credit_specification {
          - cpu_credits = "standard" -> null
        }

      - enclave_options {
          - enabled = false -> null
        }

      - metadata_options {
          - http_endpoint                  = "enabled" -> null
          - http_put_response_hop_limit     = 1 -> null
          - http_tokens                    = "optional" -> null
        }

      - root_block_device {
          - delete_on_termination = true -> null
          - device_name           = "/dev/sda1" -> null
          - encrypted             = false -> null
          - iops                  = 100 -> null
          - tags                  = {} -> null
          - throughput            = 0 -> null
          - volume_id             = "vol-06b149cdd5722d6bc" -> null
          - volume_size           = 8 -> null
          - volume_type           = "gp2" -> null
        }
   }

*Plan: 0 to add, 0 to change, 1 to destroy.    ⇽---  Terraform计划采取的操作的摘要

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

 Enter a value: 

警告 不要手动编辑或删除terraform.tfstate文件,这一点很重要,否则Terraform将无法跟踪其管理的资源。

销毁计划与前面的执行计划类似,只不过它用于删除操作。

注意 terraform destroy执行的操作相当于你删除了所有配置代码,然后运行terraform apply。

通过在命令行输入yes,确认自己希望应用销毁计划。等待几分钟,让Terraform进行处理,然后你将收到Terraform已经销毁了所有资源的通知。输出将如下所示。

aws_instance.helloworld: Destroying… [id=i-070098fcf77d93c54]
aws_instance.helloworld: Still destroying...
➥[id=i-070098fcf77d93c54, 10s elapsed]  
aws_instance.helloworld: Still destroying... ➥[id=i-070098fcf77d93c54, 20s elapsed] aws_instance.helloworld: Still destroying... ➥[id=i-070098fcf77d93c54, 30s elapsed] aws_instance.helloworld: Destruction complete after 31s Destroy complete! Resources: 1 destroyed. 

通过刷新AWS控制台,或者运行terraform show命令并确认它没有返回任何东西,验证资源确实被销毁了。

1.3 新的“Hello Terraform!”

我喜欢经典的“Hello World!”示例,并认为它是一个不错的入门项目,但我不认为它系统地展示了整个技术。Terraform不仅可以从静态配置代码置备资源,还能够基于外部查询和数据查找的结果动态置备资源。我们讨论一下数据源,该元素允许在运行时获取数据并执行计算。

本节将改进经典的“Hello World!”示例,添加一个数据源来动态查找Ubuntu AMI的最新值。我们将把输出值传入aws_instance,这样就不必在EC2实例的资源配置中静态设置AMI了(参见   图1.10)。

图1.10 aws_ami数据源的输出如何与aws_instance资源的输入连接到一起

因为我们已经配置了AWS提供程序,并使用terraform init初始化了Terraform,所以可以跳过之前的一些步骤。在这里,我们将执行下面的步骤。

(1)修改Terraform配置来添加数据源。

(2)使用terraform apply进行重新部署。

(3)使用terraform destroy进行清理。

图1.11演示了部署流程。

图1.11 部署流程

1.3.1 修改Terraform配置

我们需要添加从外部数据源读取数据的代码,以便能够查询最新发布到AWS的Ubuntu AMI。编辑main.tf,使其如代码清单1.3所示。

代码清单1.3 main.tf

provider "aws" {
  region = "us-west-2"

data "aws_ami" "ubuntu" {    ⇽---  声明一个名为“ubuntu”的aws_ami数据源
  most_recent = true

  filter {    ⇽---  设置一个过滤器,以选择名称与这个正则表达式匹配的所有AMI
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  owners = ["099720109477"]  ⇽--- 规范的Ubuntu AWS账户ID
}
resource "aws_instance" "helloworld" {
  ami           = data.aws_ami.ubuntu.id    ⇽---  将资源链接起来
  instance_type = "t2.micro"
  tags = {
    Name = "HelloWorld"
  }
}

与资源一样,要声明数据源,需要创建一个HCL对象,其类型为“data”,且具有两个标签。第一个标签指定数据源的类型,第二个标签是数据源的名称。类型和名称合起来构成了数据源的标识符,标识符在一个模块内必须保持唯一。图1.12演示了数据源的语法。

图1.12 数据源的语法

数据源代码块的内容称为“查询约束实参”。它们的行为与资源的实参的行为相同。查询约束实参用于指定从哪个(哪些)资源获取数据。数据源是不受管理的资源,Terraform能够从它们读取数据,但不能直接控制它们。

1.3.2 应用修改

接下来,我们应用修改,让Terraform部署一个使用Ubuntu数据源的输出值作为AMI的EC2实例。这需要运行terraform apply。CLI输出如下所示。

$ terraform apply

Terraform used the selected providers to generate the following execution
     plan.
Resource actions are indicated with the following symbols:
  +  create

Terraform will perform the following actions:

 # aws_instance.helloworld will be created
  + resource "aws_instance" "helloworld" {
      + ami                          = "ami-0928f4202481dfdf6"    ⇽---  使用数据源的输出进行设置
      + arn                          = (known after apply) 
      + associate_public_ip_address   = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply) 
      + cpu_threads_per_core          = (known after apply)
      + get_password_data            = false 
      + host_id                      = (known after apply)
      + id                           = (known after apply) 
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
     // skip some logs
    }

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

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

 Enter a value: 

在命令行输入yes来应用修改。等待几分钟后,输出将如下所示。

aws_instance.helloworld: Creating...
aws_instance.helloworld: Still creating... [10s elapsed]
aws_instance.helloworld: Creation complete after 19s [id=i-0c0a6a024bb4ba669]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed. 

与之前一样,可通过访问AWS控制台或者调用terraform show来验证修改。

1.3.3 销毁基础设施

运行terraform destroy,销毁上一步创建的基础设施。注意,这里仍然需要手动确认。

$ terraform destroy
aws_instance.helloworld: Refreshing state... [id=i-0c0a6a024bb4ba669]

Terraform used the selected providers to generate the following execution
     plan.
Resource actions are indicated with the following symbols:
 - destroy

Terraform will perform the following actions:

 # aws_instance.helloworld will be destroyed
  - resource "aws_instance" "helloworld" {
       - ami                          = "ami-0928f4202481dfdf6" -> null
       - arn                          = "arn:aws:ec2:us-west-2:215974853022
       ➥:instance/i-0c0a6a024bb4ba669" -> null
       - associate_public_ip_address   = true -> null
// skip some logs
    }

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

Enter a value:

手动确认并等待几分钟后,EC2实例将成功销毁。

aws_instance.helloworld: Destroying... [id=i-0c0a6a024bb4ba669]
aws_instance.helloworld: Still destroying... 
➥[id=i-0c0a6a024bb4ba669, 10s elapsed]
aws_instance.helloworld: Still destroying...
➥[id=i-0c0a6a024bb4ba669, 20s elapsed]
aws_instance.helloworld: Still destroying...
➥[id=i-0c0a6a024bb4ba669, 30s elapsed]
aws_instance.helloworld: Destruction complete after 30s

Destroy complete! Resources: 1 destroyed. 

1.4 炉边谈话

本章不仅讨论了什么是Terraform,相对于其他IaC工具它具有哪些优缺点,还讲述了如何执行两个实际部署。第一个部署是Terraform的“Hello World!”示例,第二个部署是我个人偏爱的部署,因为它使用数据源演示了Terraform的动态能力。

接下来的几章将介绍Terraform的基本工作方式,以及Terraform HCL的主要构造和语法元素。到了第4章,我们将把一个多层Web应用程序部署到AWS上。

小结


本章要点:

去掉所有修饰后,Terraform其实是一种非常简单的技术。从根本上讲,Terraform是一个状态管理工具,对其管理的资源执行CRUD(Create、Read、Update、Delete,增删改查)操作。托管的资源很多时候是基于云的资源,但也不全是。任何可以用CRUD表达的东西都可以作为Terraform资源进行管理。

本章将通过展示一个资源的生命周期深入介绍Terraform的内部原理。对于这个目的,使用任何资源都可以实现,所以我们使用一个不调用任何远程网络API的资源。这种特殊的资源称为本地资源,它们存在于Terraform或运行Terraform的机器内。本地资源通常用于完成不重要的任务,例如,将“真正的”基础设施黏合起来,但它们也是很好的教学辅助工具。本地资源的例子包括创建私钥、自签名TLS证书和随机ID的资源。

我们将使用Terraform的本地提供程序的local_file资源创建、读取、更新和删除包含《孙子兵法》前几段内容的一个文本文件。图2.1显示了《孙子兵法》场景的输入和输出。

注意 虽然一般不认为文本文件是基础设施,但仍然可以像部署EC2实例那样部署它们。这是否意味着文本文件就是真正的基础设施呢?甚至有必要做这种区分吗?这些问题的答案由你自己来决定。

首先,创建资源。然后,模拟配置漂移,并执行更新。最后,使用terraform destroy进行清理。图2.2显示了这个过程。

图2.1 《孙子兵法》场景的输入和输出

图2.2 创建资源,然后读取并更新资源,最后删除资源

所有Terraform资源都实现了资源模式接口。资源模式要求资源定义CRUD函数钩子,Create()、Read()、Update()和Delete()各有一个钩子。当满足特定条件时,Terraform将调用这些钩子。一般来说,在创建资源时会调用Create(),在生成计划时会调用Read(),在更新资源时会调用Create(),在删除时会调用Delete()。其实并不是这么简单,但你应该能够理解这里的模式。

因为local_file是资源,所以也实现了资源模式接口。这意味着,它为Create()、Read()、Update()和Delete()定义了函数钩子。这与local_file数据源不同,后者只实现了Read()(参见图2.3)。这里将指出何时以及为何会调用每个函数钩子。

图2.3 本地提供程序中的两个资源分别是管理的资源和非管理的数据源。
管理的资源实现了完整的CRUD,而数据源只实现了Read()

我们首先为Terraform创建一个新的工作空间。为此,在计算机上的某个位置新建一个空目录。确保该文件夹现在不包含任何配置代码,因为Terraform会将所有.tf文件连接起来。在这个工作空间中,创建一个名为main.tf的新文件,在其中添加代码清单2.1中的代码。

代码清单2.1 main.tf中的代码

terraform {    ⇽---  Terraform设置块
  required_version = ">= 0.15"
  required_providers {
    local = {
      source = "hashicorp/local"
      version = "~> 2.0"
    }
  }
}
resource "local_file" "literature" {
    filename = "art_of_war.txt"
    content     = <<-EOT    ⇽---  多行字符串的heredoc语法
      Sun Tzu said: The art of war is of vital importance to the State.

      It is a matter of life and death, a road either to safety or to
      ruin. Hence it is a subject of inquiry which can on no account be
      neglected.
    EOT
}

提示 <<-表示一个有缩进的heredoc字符串。开始标识符和结束标识符(EOT)之间的任何字符都按字面解释。但是,前导空格将被忽略(这与传统的heredoc语法不同)。

代码清单2.1中有两个配置块。第一个配置块terraform {…}是一个特殊的配置块,负责配置Terraform,主要用于锁定用户代码的版本号,但也可以配置状态文件的存储位置,以及从什么地方下载提供程序(第6章将详细讨论)。需要注意的是,现在还没有安装本地提供程序。要进行安装,首先需要执行terraform init。

第二个配置块是一个资源块,它声明了local_file资源。这个配置块使用给定文件名和内容值来置备一个文本文件。在这里,内容将包含《孙子兵法》的前两段,文件名则是art_of_war.txt。我们使用heredoc语法(<<-)来输入一个多行字符串字面量。

现在,因为还没有初始化Terraform,所以它并不知道你的工作空间,更不要说创建和管理任何东西了。Terraform配置必须至少初始化一次,但如果添加了新的提供程序或模块,则可能还需要再次初始化。不必担心不知道什么时候运行terraform init,因为Terraform始终会进行提醒。另外,terraform init是幂等命令,这意味着连续多次调用它也没有副作用。

现在运行terraform init。

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.0"...
- Installing hashicorp/local v2.0.0...
- Installed hashicorp/local v2.0.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the
provider selections it made above. Include this file in your version
control repository so that Terraform can guarantee to make the same
selections by default when you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to
see any changes that are required for your infrastructure. All Terraform
commands should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget,
other commands will detect it and remind you to do so if necessary. 

初始化后,Terraform会创建一个隐藏的.terraform目录来安装插件和模块。当前Terraform工作空间的目录结构如下所示。

.
![..\p56.tif{45%}](/api/storage/getbykey/original?key=220397afcbc409a9d191) .terraform
        providers
            registry.terraform.io
                hashicorp
                    local
                        2.0.0
                            darwin_amd64
                                terraform-provider-local_v2.0.0_x5
    .terraform.lock.hcl
    main.tf

7 directories, 3 files

Terraform很智能,因为我们在main.tf中声明了一个local_file资源,所以它知道存在对本地提供程序的隐式依赖。因此,Terraform将在提供程序注册表中查找并下载资源。除非你想这么做,否则不需要声明一个空的提供程序块,即provider"local"{}。

提示 对于你使用的任何提供程序,无论它们是隐式定义的还是显式定义的,都应该锁定它们的版本,以确保你的任何部署都是可重复的部署。

在使用terraform apply创建local_file资源之前,通过运行terraform plan预览Terraform准备执行的操作。在部署前,始终先运行terraform plan。为简洁起见,本书中常常跳过这个步骤,但即使我没有写出来这个命令,你也应该运行它。terraform plan会告诉你Terraform打算执行什么操作,而且它可以作为一个linter,告诉你代码中存在的任何语法错误或依赖错误。这是一个只读操作,并不会修改被部署的基础设施的状态,而且与terraform init一样,它也是幂等操作。

现在,运行terraform plan来生成一个执行计划。

$ terraform plan
Refreshing Terraform state in-memory prior to plan…
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
__________________________________________________________________________

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

Terraform will perform the following actions:

 # local_file.literature will be created
  + resource "local_file" "literature" { 
      + content             = <<~EOT
          Sun Tzu said: The art of war is of vital importance to the State.

          It is a matter of life and death, a road either to safety or to
          ruin. Hence it is a subject of inquiry which can on no account be
          neglected.
      EOT
    + directory_permission = "0777"
    + file_permission      = "0777"
    + filename             = "art_of_war.txt" 
    + id                   = (known after apply)    ⇽---  计算的元特性
}

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

Note: You didn't specify an "-out" parameter to save this plan, so
Terraform can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

计划什么时候可能失败

许多原因可能导致Terraform计划失败,例如,配置代码无效,或者存在版本问题或与网络相关的问题。有时候(尽管很少发生这种情况),计划失败的原因是提供程序的源代码中存在bug。需要仔细阅读收到的错误消息来了解失败的原因。要获得更加详细的日志,需要将环境变量TF_LOG=trace设置为一个非零值,如export TF_LOG=trace,以打开跟踪级别的日志。

从输出可见,Terraform告诉我们它想创建一个local_file资源。除我们提供的特性之外,它还想设置一个名为id的计算特性,这是Terraform会在所有资源上设置的一个元特性。该特性用于唯一标识现实世界的资源,以及完成内部计算。

虽然这里的terraform plan会很快退出,但其他计划可能需要一段时间才能完成。具体时间取决于你要部署多少资源,以及状态文件中已经有多少资源。

提示 如果terraform plan运行得很慢,则关闭跟踪级别的日志,并考虑增加并行处理(-parallelism=n)。

虽然计划的输出相当直观,但它做了许多工作,你需要了解一下。下面解释一下terraform plan的3个主要阶段。

(1)读取配置和状态。Terraform会读取配置和状态文件(前提是它们存在)。

(2)决定要采取的操作。Terraform会执行计算,确定需要执行什么操作来实现期望的状态。执行的操作包括Create()、Read()、Update()、Delete()或No-op。

(3)输出计划。执行计划确保操作按顺序发生,以避免发生依赖问题。这一点在有很多资源时尤为重要。

图2.4是一个详细的流程图,显示了在执行terraform plan时发生了什么。

尽管我们还没有介绍依赖图,但它其实是Terraform的一个重要部分,每个terraform plan都会生成一个依赖图,以遵守资源和提供程序节点之间隐式的和显式的依赖关系。Terraform有一个terraform graph命令,专门用于可视化依赖图。该命令会输出一个DOT文件,使用多种工具可以把该文件转换为一个图形。图2.5显示了生成的DOT图。

图2.4 Terraform在为新部署生成执行计划时完成的步骤

注意 DOT是一种图描述语言。DOT图是带有文件扩展名.dot的文件。许多程序能够以图形形式处理和渲染DOT文件。

图2.5 生成的DOT图

这个工作空间的依赖图有几个节点,包括对应本地提供程序的一个节点,对应local_file资源的一个节点,以及对应日常操作的一些元节点。在执行apply时,Terraform会检查依赖图,确保按正确顺序执行步骤。下一章将探讨一个更加复杂的图形。

我们可以采用JSON格式读取terraform plan的输出,这在与自定义工具集成或者实施策略即代码(第13章将讨论策略即代码)的时候很有用。

首先,通过设置可选的-out标志保存计划的输出。

$ terraform plan -out plan.out
Refreshing Terraform state in-memory prior to plan…
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
__________________________________________________________________________

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

 # local_file.literature will be created
  + resource "local_file" "literature" {
      + content             = <<~EOT
            Sun Tzu said: The art of war is of vital importance to the State.

            It is a matter of life and death, a road either to safety or to
            ruin. Hence it is a subject of inquiry which can on no account be
            neglected.
        EOT
       + directory_permission = "0777" 
       + file_permission      = "0777"
       + filename             = "art_of_war.txt" 
       + id                   = (known after apply)
}

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

This plan was saved to: plan.out

To perform exactly these actions, run the following command to apply:
terraform apply "plan.out"

现在,plan.out被另存为一个二进制文件,所以下一步是将其转换为JSON格式。这可以通过使用terraform show来获取其内容,然后传输到一个输出文件中来实现(其实不太直观)。

$ terraform show -json plan.out > plan.json

最后,我们就得到了人类可读的计划。

$ cat plan.json

{"format_version":"0.1","terraform_version":"0.15.0","planned_values":{"root
_module":{"resources":[{"address":"local_file.literature","mode":"managed",
"type":"local_file","name":"literature","provider_name":"registry.terraform.
io/hashicorp/local","schema_version":0,"values":{"content":"Sun Tzu said:
The art of war is of vital importance to the State.\n\nIt is a matter of
life and death, a road either to safety or to \nruin. Hence it is a subject
of inquiry which can on no account
be\nneglected.\n","content_base64":null,"directory_permission":"0777","file
_permission":"0777","filename":"art_of_war.txt","sensitive_content":null}}]
}},"resource_changes":[{"address":"local_file.literature","mode":"managed",
"type":"local_file","name":"literature","provider_name":"registry.terraform.
io/hashicorp/local","change":{"actions":["create"],"before":null,"after":{
"content":"Sun Tzu said: The art of war is of vital importance to the
State.\n\nIt is a matter of life and death, a road either to safety or to
\nruin. Hence it is a subject of inquiry which can on no account
be\nneglected.\n","content_base64":null,"directory_permission":"0777","file
_permission":"0777","filename":"art_of_war.txt","sensitive_content":null},"
after_unknown":{"id":true}}}],"configuration":{"root_module":{"resources":[
{"address":"local_file.literature","mode":"managed","type":"local_file","name":
"literature","provider_config_key":"local","expressions":{"content":{
"constant_value":"Sun Tzu said: The art of war is of vital importance to the
State.\n\nIt is a matter of life and death, a road either to safety or to
\nruin. Hence it is a subject of inquiry which can on no account
be\nneglected.\n"},"filename":{"constant_value":"art_of_war.txt"}},"schema_
version":0}]}}}

现在运行terraform apply,将得到的输出与生成的执行计划进行比较。命令和输出如下 所示。

$ terraform apply

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.literature will be created
  + resource "local_file" "literature" {
      + content            = <<-EOT
            Sun Tzu said: The art of war is of vital importance to the State.

            It is a matter of life and death, a road either to safety or to
            ruin. Hence it is a subject of inquiry which can on no account be
          neglected.
      EOT
    + directory_permission = "0777"
    + file_permission      = "0777"
    + filename             = "art_of_war.txt"
    + id                   = (known after apply)
}

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

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

  Enter a value:

它们看起来有些类似。这并不是偶然。terraform apply生成的执行计划与terraform plan生成的执行计划完全相同。事实上,你甚至可以显式地应用terraform plan的结果。

$ terraform plan -out plan.out && terraform apply "plan.out"

提示 当自动运行Terraform的时候(第12章将进行介绍),像这样将plan和apply分开可能很有用。

无论如何生成执行计划,在应用前先检查计划的内容总是一个好主意。在执行apply时,Terraform会创建和销毁真实的基础设施,这当然会在现实世界产生影响。如果不谨慎对待,那么一个简单的错误或者误拼也可能摧毁整个基础设施,你甚至没有机会反应。对于这个工作空间,因为我们并没有创建“真正的”基础设施,所以没有什么好担心的。

返回命令行,在提示后面输入yes来手动确认。具体输出将如下所示。

$ terraform apply
...
  Enter a value: yes

local_file.literature: Creating...
local_file.literature: Creation complete after 0s [id=df1bf9d6-c6cf-f9cb-
34b7-dc0ba10d5a1d]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed. 

此命令创建了art_of_war.txt和terraform.tfstate两个文件。现在,当前目录(不包括隐藏文件)如下所示。

.
![..\p62.tif{5%}](/api/storage/getbykey/original?key=22032b5fb14817eaa98a)   art_of_war.txt
    main.tf
    terraform.tfstate

这里的terraform.tfstate文件是一个状态文件,Terraform使用它来跟踪自己管理的资源。它用于在执行plan期间比较差异,以及检测配置漂移。该状态文件目前的内容如代码清单2.2所示。

代码清单2.2 terraform.tfstate目前的内容

{
  "version": 4,
  "terraform_version": "0.15.0",    ⇽---  关于Terraform运行情况的元数据
  "serial": 1,
  "lineage": "df1bf9d6-c6cf-f9cb-34b7-dc0ba10d5a1d",
  "outputs": {},
![箭头09{8%}](/api/storage/getbykey/original?key=2203e5abc9b68c4aa98a)  "resources": [    ⇽---  资源状态数据
    {
      "mode": "managed",
      "type": "local_file",
      "name": "literature",
      "provider": "provider[\"registry.terraform.io/hashicorp/local\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "content": "Sun Tzu said: The art of war is of vital importance
            to the State.\n\nIt is a matter of life and death, a road either to safety
            or to \nruin. Hence it is a subject of inquiry which can on no account
            be\nneglected.\n",
            "content_base64": null,
            "directory_permission": "0777",
            "file_permission": "0777",
            "filename": "art_of_war.txt",
            "id": "907b35148fa2bce6c92cba32410c25b06d24e9af",
            "sensitive_content": null,
            "source": null
          },
          "sensitive_attributes": [],
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}

警告 不要编辑、删除或破坏terraform.tfstate文件,这一点十分重要,否则Terraform可能无法跟踪它管理的资源。虽然我们能够还原损坏的或者丢失的状态文件,但这是很困难、很耗时间的操作。

通过cat文件,我们可以验证art_of_war.txt是否符合我们的期望。该命令及其输出如下所示。

$ cat art_of_war.txt
Sun Tzu said: The art of war is of vital importance to the State.

It is a matter of life and death, a road either to safety or to
ruin. Hence it is a subject of inquiry which can on no account be
neglected. 

Terraform如何创建这个文件呢?在执行apply时,Terraform调用了local_file的Create()(参见图2.6)。

图2.6 在执行terraform apply时,调用了local_file的Create()

为了理解Create()做了什么,代码清单2.3显示了提供程序的源代码。

注意 放轻松,不必担心现在理解不了代码。第11章将讨论提供程序的内部工作机制。

代码清单2.3 提供程序的源代码

func resourceLocalFileCreate(d *schema.ResourceData, _ interface{}) error {
  content, err := resourceLocalFileContent(d)
  if err != nil {
    return err
  }

  destination := d.Get("filename").(string)

  destinationDir := path.Dir(destination)
  if _, err := os.Stat(destinationDir); err != nil {
    dirPerm := d.Get("directory_permission").(string)
    dirMode, _ := strconv.ParseInt(dirPerm, 8, 64)
    if err := os.MkdirAll(destinationDir, os.FileMode(dirMode)); err != nil {
      return err
    }
  }

  filePerm := d.Get("file_permission").(string)

  fileMode, _ := strconv.ParseInt(filePerm, 8, 64)

  if err := ioutil.WriteFile(destination, []byte(content),
     os.FileMode(fileMode));
     ➥err != nil {
    return err
  }

  checksum := sha1.Sum([]byte(content))
  d.SetId(hex.EncodeToString(checksum[:]))

  return nil
}

Terraform能够读取现有资源,确保它们处于期望的配置状态。要实现这种操作,一种方法是运行terraform plan。当运行terraform plan时,Terraform会调用状态文件中的每个资源的Read()。因为此处的状态文件只有一个资源,所以Terraform只会调用local_file的Read()。图2.7显示了这一点。

图2.7 terraform plan调用了local_file资源的Read()

现在运行terraform plan。

$ terraform plan
local_file.literature: Refreshing state...
[id=907b35148fa2bce6c92cba32410c25b06d24e9af]

No changes. Infrastructure is up-to-date.

That Terraform did not detect any differences between your configuration
and the remote system(s). As a result, there are no actions to take. 

与我们期望的一样,并没有发生变化。当Read()没有返回变化时,得到的操作是一个无操作(No-operation,No-op)。图2.8显示了这一点。

代码清单2.4是提供程序中执行Read()的代码。同样,不必担心现在不能完全理解代码。

代码清单2.4 提供程序中执行Read()的代码

func resourceLocalFileRead(d *schema.ResourceData, _ interface{}) error {
    // If the output file doesn't exist, mark the resource for creation.
    outputPath := d.Get("filename").(string)
    if _, err := os.Stat(outputPath); os.IsNotExist(err) {
        d.SetId("")
        return nil
    }

    // Verify that the content of the destination file matches the content we
    // expect. Otherwise, the file might have been modified externally and we
    // must reconcile.
    outputContent, err := ioutil.ReadFile(outputPath)
    if err != nil {
        return err
    }

    outputChecksum := sha1.Sum([]byte(outputContent))
    if hex.EncodeToString(outputChecksum[:]) != d.Id() {
        d.SetId("")
        return nil
    }

    return nil
}

图2.8 当为已经处在期望状态的现有部署生成执行计划时,Terraform执行的步骤

你知道比在文件中包含《孙子兵法》的前两段更好的是什么吗?在文件中包含《孙子兵法》的前4段!更新是Terraform不可缺少的功能,所以理解其工作方式很重要。更新main.tf的代码,使其如代码清单2.5所示。

代码清单2.5 更新main.tf

terraform {
  required_version = ">= 0.15"
  required_providers {
    local = {
      source = "hashicorp/local"
      version = "~> 2.0"
    }
  }
}
 
resource "local_file" "literature" {
  filename = "art_of_war.txt"
  content     = <<-EOT
    Sun Tzu said: The art of war is of vital importance to the State.
    It is a matter of life and death, a road either to safety or to
    ruin. Hence it is a subject of inquiry which can on no account be
    neglected.

    The art of war, then, is governed by five constant factors, to be    ⇽---  添加额外的两段
    taken into account in one's deliberations, when seeking to
    determine the conditions obtaining in the field.
 
    These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
    Commander; (5) Method and discipline.
  EOT
}

并没有一个专门的命令用来执行更新,你需要做的只是运行terraform apply。不过,在运行该命令前,先运行terraform plan来看看生成的执行计划是什么样子。该命令及其输出如下所示。

$ terraform plan
local_file.literature: Refreshing state...
     [id=907b35148fa2bce6c92cba32410c25b06d24e9af]  ⇽--- Read()先发生

Terraform used the selected providers to generate the following execution
     plan.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

 # local_file.literature must be replaced
-/+ resource "local_file" "literature" {
     ~ content               = <<-EOT # forces replacement    ⇽---  force new重新创建资源
           Sun Tzu said: The art of war is of vital importance to the State.
 
           It is a matter of life and death, a road either to safety or to
           ruin. Hence it is a subject of inquiry which can on no account be
           neglected.
         + 
         + The art of war, then, is governed by five constant factors, to be
         + taken into account in one's deliberations, when seeking to 
         + determine the conditions obtaining in the field.
         + 
         + These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
         + Commander; (5) Method and discipline.
       EOT
     ~ id                   = "907b35148fa2bce6c92cba32410c25b06d24e9af"
-> (known after apply)
       # (3 unchanged attributes hidden)
    }
 
Plan: 1 to add, 0 to change, 1 to destroy.
____________________________________________________________________________
 
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now. 

可以看到,Terraform注意到我们修改了content特性,所以提议销毁旧资源,然后创建一个新资源。之所以执行这种操作,而不是就地更新特性,是因为content被标记为一个force new特性,这意味着如果修改它,整个资源就会被污染。为了得到新的期望状态,Terraform必须重建资源。这是不可变基础设施的典型示例,不过并不是Terraform管理的资源的所有特性都具有这种行为。事实上,大部分资源支持就地(即可变)更新。图2.9显示了可变和不可变更新的区别。

图2.9 可变和不可变更新的区别

“force new”更新听起来很可怕!

虽然一开始听起来,销毁然后重建被污染的基础设施这种处理让人有些不安,但terraform plan能够让我们提前知道Terraform会执行什么操作,所以并不会产生让我们意外的结果。另外,Terraform擅长创建可重复的环境,所以重建基础设施并没有问题。唯一潜在的问题是服务可能会停机。如果你完全不能容忍任何停机时间,则可以学习第9章的内容,该章介绍如何使用Terraform完成零停机时间部署。

第4章将对传输进行详细的讨论。

Terraform在生成更新的执行计划时执行的步骤如图2.10所示。

图2.10 Terraform在生成更新的执行计划时执行的步骤

通过运行命令terraform apply -auto-approve,应用执行计划提议的修改。可选的-auto-approve标志告诉Terraform跳过手动批准步骤,立即应用修改。

$ terraform apply -auto-approve
local_file.literature: Refreshing state...
     [id=907b35148fa2bce6c92cba32410c25b06d24e9af]
local_file.literature: Destroying...
     [id=907b35148fa2bce6c92cba32410c25b06d24e9af]
local_file.literature: Destruction complete after 0s
local_file.literature: Creating...
local_file.literature: Creation complete after 0s
     [id=657f681ea1991bc54967362324b5cc9e07c06ba5]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed. 

警告 如果没有事先检查计划的结果,就使用-auto-approve可能很危险。

通过再次对文件执行cat,你可以验证文件已经更新。该命令及其输出如下。

$ cat art_of_war.txt
Sun Tzu said: The art of war is of vital importance to the State.

It is a matter of life and death, a road either to safety or to
ruin. Hence it is a subject of inquiry which can on no account be
neglected.

The art of war, then, is governed by five constant factors, to be
taken into account in one's deliberations, when seeking to
determine the conditions obtaining in the field.

These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
Commander; (5) Method and discipline. 

到目前为止,我们已经能够创建和更新文本文件资源。但是,如果在Terraform外部,通过某种方式修改了文件,会发生什么?如果多个特权用户使用相同的文件系统,很容易就会发生配置漂移。如果你有基于云的资源,这相当于有人使用鼠标单击的方式在控制台中修改了部署的基础设施。Terraform如何处理配置漂移呢?它会计算当前状态和期望状态之间的差异,然后执行更新。

通过直接修改art_of_war.txt,我们可以模拟配置漂移。在该文件中,将所有“Sun Tzu”替换为“Napoleon”。

现在,art_of_war.txt的内容如下所示。

Napoleon said: The art of war is of vital importance to the
State.

It is a matter of life and death, a road either to safety or to
ruin. Hence it is a subject of inquiry which can on no account be
neglected.

The art of war, then, is governed by five constant factors, to be 
taken into account in one's deliberations, when seeking to 
determine the conditions obtaining in the field. 

These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
Commander; (5) Method and discipline. 

这个引用显然是错误的,所以我们想让Terraform检测到发生了配置漂移,并进行更正。运行terraform plan命令,看看Terraform会显示什么。

$ terraform plan
local_file.literature: Refreshing state...
     [id=657f681ea1991bc54967362324b5cc9e07c06ba5]
 
Terraform used the selected providers to generate the following execution
     plan.
Resource actions are indicated with the following symbols:
  + create
 
Terraform will perform the following actions:
 
* # local_file.literature will be created    ⇽---  这里很奇怪!
  + resource "local_file" "literature" { 
      + content              = <<-EOT
            Sun Tzu said: The art of war is of vital importance to the State.

            It is a matter of life and death, a road either to safety or to
            ruin. Hence it is a subject of inquiry which can on no account be
            neglected.
 
            The art of war, then, is governed by five constant factors, to be
            taken into account in one's deliberations, when seeking to
            determine the conditions obtaining in the field.
 
            These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
            Commander; (5) Method and discipline.
        EOT
      + directory_permission = "0777" 
      + file_permission      = "0777"
      + filename             = "art_of_war.txt" 
      + id                   = (known after apply)
    }
 
Plan: 1 to add, 0 to change, 0 to destroy.
____________________________________________________________________________

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now. 

等一下,这里发生了什么呢?Terraform似乎忘记了它管理的资源,而提议创建一个新的资源。事实上,Terraform并没有忘记它管理的资源——状态文件中仍然包含该资源,这可以通过运行terraform show来进行验证。

$ terraform show
# local_file.literature:
resource "local_file" "literature" {
    content = <<-EOT
        Sun Tzu said: The art of war is of vital importance to the State.

        It is a matter of life and death, a road either to safety or to
        ruin. Hence it is a subject of inquiry which can on no account be
        neglected.
        The art of war, then, is governed by five constant factors, to be 
        taken into account in one's deliberations, when seeking to 
        determine the conditions obtaining in the field. 

        These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
        Commander; (5) Method and discipline.
    EOT
    directory_permission = "0777"
    file_permission      = "0777"
    filename             = "art_of_war.txt"
    id                   = "657f681ea1991bc54967362324b5cc9e07c06ba5"
}

terraform plan得到了令人奇怪的结果,只不过是因为提供程序选择实现Read()的方式有点奇怪。我不知道为什么提供程序选择这么做,但如果文件内容不能精确匹配状态文件中的内容,则提供程序就认为该文件不再存在。结果就是,尽管仍然存在同名的文件,但Terraform认为资源不再存在。这一点在执行apply时并不重要,因为现有文件将被重写,但无论如何,这是一种奇怪的行为。

如何修复配置漂移呢?如果你运行terraform apply,Terraform会自动修复配置漂移,但我们现在先不那么做,而是让Terraform协调它知道的状态和当前部署的状态。这可以通过terraform refresh命令来实现。

可以把terraform refresh想象为修改状态文件的terraform plan。它是只读操作,仅修改Terraform状态,而不会修改管理的现有基础设施。

返回命令行,运行terraform refresh来协调Terraform状态。

$ terraform refresh
local_file.literature: Refreshing state...
     [id=657f681ea1991bc54967362324b5cc9e07c06ba5] 

现在,如果运行terraform show,会看到状态文件已更新。

$ terraform show

但是,什么也没有返回,这就是local_file工作方式奇怪的地方(它认为旧文件不再存在)。至少现在,行为是一致的。

注意 我很少发现terraform refresh有用,但一些人非常喜欢使用这个命令。

返回命令行,使用terraform apply来纠正art_of_war.txt文件。

$ terraform apply -auto-approve
local_file.literature: Creating...
local_file.literature: Creation complete after 0s
     [id=657f681ea1991bc54967362324b5cc9e07c06ba5]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed. 

现在,art_of_war.txt的内容已经恢复正确。如果这是在Amazon Web Services(AWS)、Google Cloud Platform(GCP)或Azure中置备的基于云的资源,那么此时,在控制台中使用鼠标做的任何修改都将被撤销。通过再次对文件执行cat,我们可以验证文件已经成功恢复。

$ cat art_of_war.txt
Sun Tzu said: The art of war is of vital importance to the State.

It is a matter of life and death, a road either to safety or to
ruin. Hence it is a subject of inquiry which can on no account be
neglected.

The art of war, then, is governed by five constant factors, to be
taken into account in one's deliberations, when seeking to
determine the conditions obtaining in the field.

These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
Commander; (5) Method and discipline. 

《孙子兵法》文件起到了它的作用,但现在是时候跟它说再见了。运行terraform destroy命令来进行清理。

$ terraform destroy -auto-approve
local_file.literature: Refreshing state...
     [id=657f681ea1991bc54967362324b5cc9e07c06ba5]
local_file.literature: Destroying...
     [id=657f681ea1991bc54967362324b5cc9e07c06ba5]
local_file.literature: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed. 

注意 terraform destroy命令的可选标志-auto-approve与terraform apply的对应标志完全相同,它会自动批准执行计划的结果。

terraform destroy命令首先生成一个执行计划,就像配置文件中没有资源一样。它会对每个资源执行Read(),然后把所有现有资源标记为需要删除。图2.11显示了Terraform在生成删除资源的执行计划时执行的步骤。

在实际执行destroy操作时,Terraform会对状态文件中的每个资源调用Delete()。同样,因为状态文件中只有一个资源,所以Terraform实际上只会对local_file调用Delete()。图2.12演示了这一点。

现在art_of_war.txt文件就被删除了。当前目录如下所示。

.
![..\p62.tif{5%}](/api/storage/getbykey/original?key=22037827e3dab823bdde)    main.tf
     terraform.tfstate
     terraform.tfstate.backup

图2.11 Terraform在生成删除资源执行计划时执行的步骤

图2.12 terraform destroy对状态文件中的每个资源调用Delete()

注意 删除全部配置文件,然后运行terraform apply命令,相当于运行terraform destroy命令。

虽然文件已经删除,但其内容仍然保存在新文件terraform.tfstate.backup中。这个备份文件是前面的状态文件的副本,纯粹用于归档。通常用不到这个文件,所以如果愿意,你可以删除它,但我通常保留它。如代码清单2.6所示,在Terraform看来,目前的状态文件terraform.tfstate是空的。

代码清单2.6 terraform.tfstate

{
  "version": 4,
  "terraform_version": "0.15.0",  
  "serial": 9,  
  "lineage": "df1bf9d6-c6cf-f9cb-34b7-dc0ba10d5a1d",  
  "outputs": {},  
  "resources": []  
}  

最后,为了帮助大家成长,代码清单2.7列出了本地提供程序的Delete()代码(它相当简单)。

代码清单2.7 本地提供程序的Delete()代码

func resourceLocalFileDelete(d *schema.ResourceData, _ interface{}) error {
    os.Remove(d.Get("filename").(string))
    return nil
}

本章介绍了Terraform的内部原理,讲解了其工作方式、如何置备基础设施,以及如何计算差异。Terraform本质上是一个状态管理工具,用于对管理的资源执行CRUD操作。在云环境中,因为云本身已经很神奇,所以这有些让人困惑,但其实并没有看起来那么困难。Terraform使用的API与你自己编写一个自动化脚本来部署基础设施时使用的API相同。区别在于,Terraform不仅部署基础设施,还管理基础设施。Terraform理解资源之间的依赖,甚至能够检测并校正配置漂移。Terraform是一个简单的状态管理引擎,其价值主要来自Terraform注册表上发布的众多提供程序。下一章将介绍两个新的提供程序——Random和Archive提供程序。


相关图书

有限元基础与COMSOL案例分析
有限元基础与COMSOL案例分析
程序员的README
程序员的README
现代控制系统(第14版)
现代控制系统(第14版)
现代软件工程:如何高效构建软件
现代软件工程:如何高效构建软件
GitLab CI/CD 从入门到实战
GitLab CI/CD 从入门到实战
科学知识图谱:工具、方法与应用
科学知识图谱:工具、方法与应用

相关文章

相关课程