Terraform for Tencent Cloud

前言

腾讯云已经支持Terraform作为基础设施即代码(IaC)工具。因此,对于任何在腾讯云上工作的云计算架构师和工程师来说,对Terraform的良好理解是必不可少的。然而,目前还没有专门的教学资源来关注如何在腾讯云上使用Terraform。

本书是第一本专门针对腾讯云向读者教授Terraform的书。我将带你了解从基本概念到使用Terraform部署复杂架构的过程。通过简单易懂的代码实例,我将告诉你如何在腾讯云中对Terraform进行身份验证,教你适用于腾讯云的Terraform语言的所有基本概念,并实现“一键部署”完整的工作架构。我们还将向你展示如何使用腾讯云原生工具和第三方工具改善你的Terraform工作流程。

在本书结束时,你将对Terraform以及如何在腾讯云上使用它有一个全面的了解。你将学会如何开发有效的Terraform代码,构建可复用的模块,并利用公共开源的Terraform模块,更快、更安全地在腾讯云上进行部署。

本书适用对象

如果你在工作中使用腾讯云,并希望在配置和管理腾讯云基础设施方面变得更有效率,这本书就是为你准备的。我们教你IaC的原理,并指导你如何使用Terraform作为腾讯云的IaC工具。我们将带你走过一段旅程,从使用单个文件配置虚拟机到在不同的环境中部署多层架构,只需一键执行。

本书的内容

第一章,腾讯云上的Terraform入门,介绍了IaC的概念,然后使用两种方法在腾讯云上认证Terraform,配置虚拟机。

第二章,探索Terraform,详细介绍了Terraform如何使用状态信息来决定采取什么行动。它还解释了元参数的使用,这使你能够编写有效的Terraform代码。

第三章,编写高效的Terraform代码,介绍了具体的Terraform结构,帮助你开发高效的Terraform代码,并解释了如何暴露Terraform状态信息。

第四章,使用模块编写可复用的代码,解释了如何开发Terraform模块来复用和共享Terraform代码。

第五章,管理多环境,比较了使用同一个Terraform代码库来管理多个环境的两种主要方法。

第六章,部署传统的三层架构,使用到目前为止介绍的概念来构建一个完整的三层架构。

第七章,使用公共模块部署TKE,介绍了如何使用两个最常见的公共模块来部署开发和生产TKE集群,只使用不同的变量分配。

第八章,部署无服务器架构,继续应用所学的概念来部署一个云原生的、完全无服务器的架构。

第九章,高效地开发Terraform代码,介绍了四种最常用的工具,以改善你的Terraform工作流程。

第十章,腾讯云整合,展示了如何使用云构建为Terraform创建一个CI/CD管道,并介绍了一个独特的腾讯云功能,将现有的云资源导入Terraform的领域中。

为了充分利用这本书

你应该对腾讯云有基本的了解,包括如何使用腾讯云控制台和腾讯云命令行工具tccli来配置腾讯云资源。我们使用macOS和zsh shell,但Terraform和tccli在Ubuntu上使用bash,在Windows上使用PowerShell也同样适用。

下载示例代码

您可以从GitHub下载本书的示例代码文件:https://github.com/netcmcc/terraform-for-tencentcloud-code. 如果代码有更新,它将在GitHub仓库中更新。

排版约定

本书中使用了一些文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、假的URL、用户输入。下面是一个例子:"按照惯例,我们有三个文件:main.tf,包含主代码;variables.tf,定义需要传递给模块的变量;output.tf,包含传回给调用模块的信息。"

一个代码块如下:

 

任何命令行的输入或输出都写成这样:

 

粗体:表示一个新的术语、一个重要的词,或你在屏幕上看到的词。例如,菜单或对话框中的单词会以粗体显示。下面是一个例子:"使用腾讯云控制台,转到服务器,并查看..."

提示或重要说明:

像这样出现。

联系

我们随时欢迎读者提供反馈。如果你对本书有任何方面的疑问,请给我发电子邮件:netcmcc@gmail.com,并在邮件的主题中提到本书书名。

 

第一部分:学习基础知识

第一部分涉及腾讯云Terraform的基本原理。我们首先概述了基础设施即代码和Terraform,然后展示了用腾讯云验证Terraform的四种方法。我们介绍了Terraform的工作流程,并深入探讨了Terraform的状态文件,这对了解Terraform的运作方式至关重要。第三章介绍了Terraform语言的一些独特概念,这有助于编写Terraform代码。第四章介绍了Terraform模块,这样你就可以复用和分享Terraform代码,并利用公共Terraform模块。在Terraform中,有两种主要的方法来管理多个环境,如开发、测试和生产环境。第五章详细介绍了这两种方法,并讨论了每种方法的优点和缺点。

在整个第一部分中,我们使用简单但真实的代码例子来集中介绍概念。读完第一部分后,你将对Terraform语言及其内部运作有一个全面的了解,因此你可以使用Terraform在腾讯云中建立复杂的部署。

本书的这一部分包括以下各章:

 

第一章 在腾讯云上使用Terraform

让我们先简单介绍一下DevOps和基础设施即代码(IaC) 在这个现代的软件开发实践中的核心作用。然后,我们将讨论为什么Terraform已经成为IaC工具的事实标准,以及为什么了解Terraform对任何有追求的云计算工程师和云架构师来说是必不可少的。之后,你将学习如何使用Terraform来配置腾讯云的资源。

因此,在本章结束时,你将更好地理解为什么要使用Terraform来配置云基础设施。你也将学会如何验证Terraform,并使用Terraform配置你的第一个腾讯云CVM服务器。

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

技术要求

本章和本书的其它章节,要求你有一个腾讯云账户。你可以使用现有的腾讯云账号,如果你正在使用公司的腾讯云账号并且权限受到一定限制,我们建议你注册一个个人的腾讯云账号,以便你跟随本书进行动手实践。

请确保腾讯云账户内有一定余额(建议不低于100元)以便能够成功创建云资源。本书动手实验采用按量付费模式采购较小规格的实例,如果你在每次学习结束后及时销毁云资源,完成本书全部实验的花费通常在100元以内。

你还应该在你的本地PC上安装Terraform。Terraform在所有常见的操作系统上都有,而且很容易安装。你可以在https://www.terraform.io/downloads下载适合你的操作系统的版本。HashiCorp通过提供更新和升级不断改进该工具。在本书的写作中,我们使用的是V1.3.6版。该代码应该可以在大于v.1.3.6的任何版本中工作。然而,如果你遇到任何问题,我们建议你下载这个特定的版本。

本章和所有其他章节的源代码可从https://github.com/netcmcc/terraform-for-tencentcloud-code获取。

我们建议你下载代码,使你能够动手实践。我们按照章节和小节组织代码,并标明相应的子目录。

DevOps的崛起

自2000年中期以来,云计算的崛起令人瞩目。几乎每个月都有三个超大规模的公司(亚马逊网络服务(AWS)、Azure和谷歌云宣布开设一个新的数据中心区域。云计算--尤其是公有云--以几乎无限的规模提供了一系列令人难以置信的技术。这导致了一种新的部署和运营IT的方式。DevOps结合了传统上软件开发中不同阶段的两个领域--开发和运营。DevOps旨在以比传统瀑布模型更快的速度交付软件。通过结合历史上不同的阶段和团队,DevOps方法可以比传统的软件方法更快地交付软件,而且质量更高。

DevOps的一个关键方面是自动化。将几个独立的工具结合成一个管道,我们可以在最小的人为干预下将软件从开发到生产。这种持续集成和持续交付的概念,通常被称为CI/CD,即将管理源代码、配置IT基础设施、编译(如果需要)、打包、测试和部署整合到一个管道中。一个CI/CD管道需要在每一个步骤中实现自动化,以便有效执行。

基础设施即代码

IT基础设施的自动化配置是CI/CD管道的一个关键组成部分,被称为IaC。在传统的内部计算中,服务器和网络基础设施的供应是一个漫长的手工过程。它始于订购IT硬件,硬件的物理设置和配置,如安装操作系统和配置网络基础设施。这个过程往往需要数周或数月。虚拟化在一定程度上帮助了这一过程,但在开发人员开始将他们的劳动成果部署到测试环境之前,基础设施的配置通常仍会落在一个单独的团队身上,更不用说生产环境。

云计算的兴起将这个过程从几个月缩短到几天甚至几小时。基础设施现在可以通过一个被称为Web控制台的图形用户界面(GUI)进行配置。最初,这是一个简单的过程,因为服务和配置选项的数量是可控的。然而,随着更多服务的出现和配置选项的成倍增加,配置网络、服务器、数据库和其他管理服务变得繁琐和容易出错。

DevOps的一个关键目标是要有基本相同的环境。也就是说,除了一些小的配置变化,开发、测试和生产环境应该是相同的;例如,除了CPU的数量和内存的大小,开发和生产环境中的数据库应该是相同的。

然而,使用Web控制台配置复杂的环境是乏味和容易出错的。使用有时被嘲笑为ClickOps的方法,即使是一个中等复杂环境的配置也需要数百次,甚至数千次的用户互动或点击。

使用CLI可以帮助配置多种环境。人们可以开发脚本,将多个CLI命令合并为一个可执行文件。然而,使用CLI来管理和进行修改几乎是不可能的。

进入IaC。IaC是指使用代码而不是Web控制台或CLI来配置基础设施。例如,你可以编写代码来实现同样的步骤,而不是使用Web控制台和通过几个手动步骤来配置服务器。例如,本章中的main.tf文件显示了一个使用Ubuntu操作系统的服务器的配置。该服务器被命名为cloudshell,是一个放置在ap-guangzhou-3区域的S5.MEDIUM2实例 。

一旦你有了代码,你可以重复使用它。也就是说,你可以用最小的改动或不改动来部署许多服务器。由于,它是常规代码,你可以使用版本控制和源代码修订系统来管理你的代码。这有利于有效的团队工作。

此外,IaC可以被集成到CI/CD管道中,包括测试和验证。也就是说,你可以在服务器部署之前验证和测试其配置。因此,你可以在几分钟内而不是几天或几周内配置涉及数百台服务器、复杂网络和多种服务的复杂基础设施。最终,这使得软件开发的发布过程更快、更安全,这正是DevOps的目标。

除了IaC,CI/CD管道还包括许多步骤。其中一个步骤是配置管理,包括配置服务器,如安装更新、库和代码。Ansible、Puppet和Chef是常见的配置管理工具。配置管理工具和IaC工具之间存在一些重叠。配置管理工具可以配置一些基础设施,而IaC工具则执行一些配置任务。然而,基础设施配置和配置管理通常应被视为两个独立的步骤,最好由不同的工具提供服务。

Terraform

Terraform已经成为近年来最流行的IaC工具。虽然每个云平台都有其专有的IaC工具(AWS有CloudFormation,Azure有Azure Resource Manager,Google Cloud有Deployment Manager),但Terraform的独特之处在于它与平台是无关的。你可以使用Terraform在任何云供应商和许多其他平台(如vSphere)配置基础设施。

正如你将看到的,Terraform很容易学习,并提供了一个直接的工作流程,可以很容易地纳入任何CI工具。Terraform是开源的,已经形成了一个活跃的社区,有自己的生态系统。社区成员已经开发了一些工具,帮助你更有效地编写Terraform代码;我们在本书中介绍了其中一些工具。

在腾讯云Shell中运行Terraform

本节的代码在本书的GitHub repo中的chap01/cloudshell下。

在腾讯云中运行Terraform最简单的方法是使用Cloud Shell。这是一个预先配置的开发环境,可以通过浏览器访问。它预装了最新版本的通用工具,包括最新版本的Terraform。此外,所有的认证都已经设置好了。让我们试一试吧。

Cloud Shell是网页版命令行工具,帮助你通过命令行管理腾讯云资源。你可以通过浏览器启动Cloud Shell,启动时将会自动为你分配一台Linux服务器供你免费使用,该实例上已经预装TCCLI及Terraform等多种云管理工具和SSH及VIM等系统工具。

登陆Cloud Shell:https://iaas.cloud.tencent.com/cloudshell

你可以使用terraform -version命令检查Terraform的当前版本。

image-20230621111725670

然后使用文本编辑器例如`vim,在当前目录下,将以下代码保存为一个名为main.tf的文件中。这就是所谓的Terraform配置。配置是用HashiCorp配置语言(HCL)编写的。HCL是人类可读的,并被一些HashiCorp工具所使用。

在本书中,我们将使用术语Terraform语言来指代编写配置文件的语言。

本书旨在让你熟练掌握Terraform,以编写高质量、可复用的基础设施代码,从而高效、安全地配置腾讯云中的资源。

文件名可以是任意的,但注意.tf扩展名是必须的。Terraform使用.tf扩展名来识别Terraform文件。

main.tf

 

接下来,运行以下两个命令:

 

Terraform会问你是否执行这些操作,所以输入yes来批准。

 

恭喜你,你现在已经使用Terraform配置了你的第一台服务器。转到云服务器控制台,切换地域为广州,检查Terraform创建的实例。

正如预期的那样,Terraform创建了一台名为cloudshell,机型为SA1的云服务器,位于广州三区:

image-20230623143031728

图1.1 - 腾讯云控制台中Terraform配置的服务器。

让我们更详细地了解一下如何编写Terraform配置以及Terraform的运作方式。

Terraform语言

如前所述,Terraform语言是基于HCL的。最基本的结构是一个块 (block),块的定义为 "其他内容的容器"。例如,前面的代码块是属于资源类型的。一个资源块有两个标签。第一个标签由Provider和资源名称组成。例如,腾讯计算引擎的资源标签是tencent_compute_instance,而AWS EC2实例的标签是aws_instance。资源块中的第二个标签是ID,是你给的名称,以便Terraform能够唯一地识别每个资源。请注意,没有两个资源可以有相同的名字。因此,如果你想配置两个虚拟机,你要定义两个资源块,每个都是tencent_compute_instance类型,并且每个都有一个唯一的名字。

块的主体被嵌套在"{}"中。块可以有嵌套的块。在前面的代码例子中,有两个嵌套块,boot_disk(它本身就有一个嵌套块,叫做initialize_params)和 network_interface。

虽然Terraform的语法是相同的,但每个云提供商都有其独特的资源 (Resources)数据源 (Data Sources) 定义。腾讯云的Terraform Provider文档在https://registry.terraform.io/providers/tencentcloudstack/tencentcloud/latest/docs。你可以看到,所有的资源和数据源都是以tencentcloud前缀开始的。一般来说,它的后面是类似于云产品的名称。因此,创建一个私有网络 (VPC) 的资源被命名为tencentcloud_vpc,而提供一个MySQL数据库实例的资源被命名为 tencentcloud_mysql_instance。

看一下文档,你可以看到目前支持的腾讯云服务。随着腾讯云推出新的服务,会有更多的服务加入。我们建议你把文档链接收藏起来,因为你必须经常参考它。

在本书的第一部分,我们只使用了一些腾讯云资源,因为我们想集中精力在语言上。在第二部分中,我们使用额外的腾讯云服务来设计复杂的架构。但首先,让我们看看使用Terraform的步骤。

Terraform工作流程

一个基本的Terraform工作流程包括以下两个步骤:

  1. 初始化 (init)
  2. 应用 (apply)

在这第一步中,terraform init初始化Terraform并下载所有必要的配置文件。你看,init运行后,有一个名为.terraform的隐藏目录。这个目录是Terraform存储各种配置文件的地方。你必须在一个新目录中或者在编写代码时运行初始化步骤。不要担心,你可以多次运行这个命令,随着时间的推移,你会知道什么时候需要重新运行它。如果有疑问,再运行一次,因为它只需要几秒钟。

第二步,terraform apply包括两个阶段。首先,Terraform创建一个执行计划,然后应用这个执行计划。执行计划是Terraform将执行哪些行动,以何种顺序执行的一个内部计划。Terraform输出这个计划,包括多少资源将被添加、更改和销毁的摘要。你应该始终仔细审查这个输出。一旦你确认该计划将按照你的预期进行,你就可以确认,然后Terraform实际去变更这些云资源。

你也可以通过运行terraform plan,将计划保存在一个文件中,然后针对该文件运行terraform apply,明确地运行这两个阶段,就像这样:

 

这通常是在CI/CD管道中进行的,我们将在后面回到这个问题。

IaC的优点之一是你可以快速删除资源。这不仅在开发阶段很有用,因为你可以快速测试东西,而且还可以帮助你节约云成本。在工作日结束时,你可以删除所有的资源,并在第二天重新配置它们。要在Terraform中销毁资源,只需执行以下命令:

 

现在,在你运行terraform destroy和运行两次terraform apply后,你会发现第二次运行terraform apply时,Terraform没有报告任何变化,也没有执行任何行动。下一章将讨论Terraform如何决定采取什么行动。

还有很多Terraform命令,我们会在书中介绍它们。现在,请记住以下四个命令:

现在我们已经描述了Terraform的基本工作流程,并展示了如何在腾讯云Shell中运行Terraform,让我们来看看在本地计算机上运行Terraform的不同方法。

在本地环境中运行Terraform

虽然在Cloud Shell中运行Terraform是最简单的方式,但大多数开发者更喜欢在自己的机器上在本地环境中编辑和开发代码。

首先,在你的电脑上安装Terraform。Terraform适用于大多数平台,包括Linux、macOS和Windows。要安装Terraform,请转到https://developer.hashicorp.com/terraform/downloads,按照你的环境的安装说明进行安装。

在本书中,我们在macOS上使用Terraform,但不管是哪种操作系统,其代码和原理都是一样的。

要想知道你是否已经成功地安装了Terraform,请在你的本地机器上用你喜欢的终端 (Terminal) 运行命令:

 

一旦你成功地安装了Terraform,你需要针对你所使用的腾讯云项目对Terraform进行认证。

使用API密钥进行认证

本节的代码在chap01/api-key中 。

有几种方法来验证Terraform与腾讯云的关系。第一种是使用子用户。为此,你要创建一个子用户并保存密钥信息。

您可以使用腾讯云控制台,转到访问管理,创建一个子用户并生成API访问密钥。

点击用户列表,新建用户,快速创建。

点击创建用户,完成创建。

image-20230623135944061

成功新建用户后,请下载或复制密钥并将其妥善保存。

请注意,为了简单起见,我们给了Terraform子用户一个广泛的权限--AdministratorAccess。这并不推荐,因为它违反了最小权限的安全原则。

正如我们提到的,Terraform并不关心文件名。它将所有以.tf为扩展名的文件结合起来,然后使用所有的配置创建一个执行计划。然而,虽然Terraform不关心文件名,但人类关心。由于IaC的目标之一是共享和复用代码,因此有一些普遍采用的文件命名惯例。我们在本书中使用这些约定。

惯例是将Provider的声明放在一个叫做provider.tf的文件中。我们在前面提到,Terraform支持几个云提供商和其他基础设施工具。它通过使用Provider的概念来做到这一点。Provider是Terraform和Provider的外部API之间的一个翻译层。当你运行Terraform时,它将你的配置代码翻译成API调用,与供应商--本例中是腾讯云--进行交互。如果你用AWS做为提供商运行Terraform,它就会产生AWS的API调用。

这也是为什么虽然Terraform适用于所有的云供应商,但每个配置文件都是针对特定的云供应商的。你不能用同样的代码来配置腾讯云和AWS的虚拟机。然而,开发和管理Terraform代码的原则和工作流程对所有的云供应商都是相似的。因此,如果你已经掌握了腾讯云的Terraform,你可以用这些知识来学习AWS和Azure的Terraform。

腾讯Provider有几个可选的属性。最常用的属性是项目、地区和区域,它们指定了默认的项目、地区和区域。因此,我们的例子中使用当前目录中的凭证文件的Provider文件看起来像这样:

provider.tf

 

因此,你现在可以在你的本地计算机上运行Terraform工作流程,像这样:

 

API密钥在腾讯云中非常强大,必须谨慎对待。因此,我们不建议将密钥文件存储在版本控制系统(比如GitHub仓库)中。这很容易意外地将密钥包含在你的源代码中,在那里它可能会泄漏。这也会使代码的可移植性降低。

腾讯云Provider镜像加速

在中国境内执行terraform init有可能会较慢或者超时失败,这是因为Terraform Registry在国外,有时通过手动重试可以解决,也可配置腾讯云官方的Provider镜像。

使用文本编辑器,在家目录 (Home directory) 创建一个Terraform全局配置文件.terraformrc

 

文件内容如下:

.terraformrc

 

因为这个镜像URL只包含腾讯云的Provider,随着我们学习的深入,引入更多的Provider时这个方法就不奏效了,拉取其他Provider时仍然会较慢或者超时失败。

当我们使用多种Provider时想要提速,有两种解决方法。一种是在执行terraform init时使用代理服务器,另一种方式是使用对象存储桶自己创建一个镜像,包含所有需要用到的Provider。我们将在附录中介绍这个主题。

使用环境变量进行认证

本节的代码在chap01/environment-variable中 。

一个更好的方法是将密钥内容存储在环境变量中,反过来,它可以比磁盘上的明文密钥文件更安全地存储(例如,通过envchain),正如在下面的命令中可以看到:

 

使用环境变量还有一个好处,就是使你的Terraform代码更具可移植性,因为它不包括Terraform代码中的认证方法。

现在我们已经展示了将Terraform与腾讯云进行认证的不同方法,并创建了一个简单的云服务器,让我们提供更有用的东西。但在我们进入下一节之前,不要忘记通过运行以下命令来销毁服务器:

 

Terraform的参数化

本节的代码在chap01/parameterizing-terraform中 。

到目前为止,我们已经配置了一台服务器,它并没有真正做什么。为了结束这第一章,让我们对它进行扩展,以展示IaC的力量。首先,我们添加变量,使我们的代码更加通用。在Terraform中,你需要在一个变量块中声明变量。虽然你可以在代码的任何地方声明变量,但按照惯例,最好是在一个叫做variables.tf的文件中声明。变量声明不需要参数,但最好是定义类型和描述,如果有用的话,还可以添加一个默认值。你也可以添加验证,并指定该变量包含一个敏感值,但后面会详细介绍:

variables.tf

 

变量的引用使用var.<variable_name>语法。因此,我们的参数化main.tf文件现在看起来像这样:

main.tf

 

我们对tencentcloud_instance资源做了两个小补充。我们增加了分配公网IP地址相关的代码,包括分配公网IP的开关allocate_public_ip,以及配置指定公网带宽internet_max_bandwidth_out,带宽计费类型internet_charge_type的配置。

请注意,由于我们尚未给实例指定安全组,所以腾讯云会为实例绑定一个名为"default"的默认安全组,该安全组默认允许所有入站和出站流量,无论是在生产还是测试环境都不推荐这样做。稍后,我们将展示如何使用Terraform创建和管理安全组及规则。

有多种方法可以为变量赋值。首先,你可以在声明中指定一个默认值。其次,你可以以交互方式或通过命令行参数将其作为一个值传递。指定变量值的常见方法之一是将变量定义在.tfvars文件中。这个文件只包含变量=值形式的变量赋值。你将在第三章中了解tfvars的用处:

terraform.tfvars

 

我们做的第二个补充是自动配置一个HTTP服务器。正如我们前面所说,IaC和配置管理之间有一些重叠。你可以启动脚本,在服务器部署后执行一些配置代码。配置管理工具提供的功能要多得多,你应该只用启动脚本进行基本配置。另外,你也可以使用启动脚本来运行配置管理工具。我们的启动脚本安装了Apache网络服务器,并添加了必须的"Hello World"文本:

startup.sh

 

最后,我们可以使用输出块来输出服务器的公共IP地址(不用担心语法问题,我们稍后会详细说明)。同样,我们使用惯例将输出块放在一个名为output.tf的文件中:

output.tf

 

因此,你的当前目录下应该有以下文件:

 

Terraform创建服务器,然后输出其IP地址。复制该IP地址并粘贴到浏览器中。你将会看到一个Hello World页面。如果返回超时错误,可以1-2分钟后重试,因为运行启动脚本需要时间。

虽然我们将默认的机型定义为S5.MEDIUM2,但我们可以在命令行上使用- var标志覆盖任何变量值。

因此,下面的命令将会创建一台类似的服务器,除了机型使用SA1.MEDIUM4:

 

在我们结束这一章时,记得清理你的环境并删除你不再需要的服务器和资源。如果你还没有这样做,请运行以下命令:

 

确认你使用Web控制台删除了所有不必要的服务器也是一个好的做法。

比较认证方法

本章介绍了在腾讯云中运行和认证Terraform的三种不同方法。

管理API密钥会带来安全风险。密钥不是自动轮换的,因此往往是长期存在的。定期手动轮换API密钥是推荐的做法,虽然这带来了一定的开销。

摘要

在本章中,我们介绍了IaC的概念以及为什么它对DevOps至关重要。IaC使云资源的配置更快、更一致,也更安全。有几个IaC工具,但Terraform已经成为IaC工具的事实标准,因为它很容易使用,而且通过Provider的概念,可以用于任何云厂商。

你在腾讯云 Shell中使用Terraform配置了你在腾讯云中的第一个资源。但由于大多数云计算工程师和架构师更喜欢使用自己的机器,你学会了三种认证方法,可以在本地PC上使用Terraform。

本章还介绍了变量,使Terraform的配置更加灵活,并展示了为变量赋值的不同方法。

在下一章中,你将深入了解Terraform的内部工作原理,讨论Terraform状态的概念,并介绍Terraform语言的其他概念,从而加深对Terraform的理解。

在你进入下一章之前,确保你已经删除了所有的服务器,不要产生任何不必要的费用。IaC和Terraform的好处是,如果你再次需要资源,你只需要运行Terraform来重新创建。

 

第二章 探索Terraform

在本章中,我们将继续探索Terraform。我们首先解释了Terraform的状态,以及Terraform如何利用它来计划它所需要采取的行动。我们展示了关于如何调查当前状态的各种命令,需要注意的事项,以及团队如何通过使用后台状态进行协作。

我们将研究元参数,这是特殊的Terraform结构,使你能够编写高效的Terraform代码。

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

技术要求

我们假设你现在有一个安装了Terraform和tccli的本地环境。

本章的源代码位于我们GitHub仓库的chap02中,正如我们在前一章所做的那样,我们将每一节的代码放在相应的子目录中。

了解Terraform状态

本节的代码在本书的GitHub repo中的chap02/state-file下。

理解状态的概念对于掌握Terraform至关重要。Terraform配置是对所需状态的声明,也就是说,你指定了你想让事情变成什么样。Terraform的工作就是把当前的状态变成所期望的状态

所以,当你开始的时候,你在你的配置中--也就是.tf文件的集合中--指定你想拥有一个计算实例。然后,当你运行terraform apply时,Terraform采取必要的行动,将资源地址带入所需的状态。由于你的配置文件中没有指定任何计算实例,Terraform创建了一个。

为了开始这一部分,通过执行以下两个命令来运行Terraform。这提供了我们在上一章中留下的基础设施:

 

现在,再次运行terraform应用。Terraform确定了实际状态和期望状态之间的差异。由于实际状态映射到了期望状态,Terraform不需要采取任何行动。

Terraform将现有状态存储在Terraform状态文件中。默认情况下,它被存储在当前目录下一个名为terraform.tfstate的文件中。继续看一下。由于它是JSON格式,所以是人类可读的。对于这个单一的资源,它超过了100行;对于稍微复杂的部署,状态文件会达到1000行。一般来说,你不应该需要直接阅读或操作状态文件。

看一下状态文件,你发现它以一些元数据开始。然后它列出了输出和资源。对于这个单一的计算实例资源,它列出了所有的属性。

由于JSON文件只是该状态的内部表示,并不意味着可以被人类编辑或阅读,Terraform提供了几个命令,使状态文件更容易访问,操作更安全。terraform show以人类可读的格式打印出当前的状态文件(或计划文件),但它通常非常冗长,特别是当你配置了多个资源时。这个单一资源的完整状态文件表述超过70行。

Terraform状态列表命令更实用。它列出了状态文件中的所有资源。在我们的例子中,它显示了一个资源地址为tencentcloud_instance.this的单一资源。你注意到,资源地址是资源类型(tencentcloud_instance)和名称(this)的连接。

terraform state show ADDRESS命令显示了该资源的所有属性。因此,在我们当前的状态下,我们可以看到一个资源,其资源地址为tencentcloud_instance.this:

 

Terraform使用资源地址来唯一地识别资源。例如,tencentcloud_instance.this标识了我们在配置中指定的单一服务器。对于资源规范,其语法是resource_type.resource_name[instance_index]。

其中resource_type是资源的类型--即tencentcloud_instance,resource_name是我们给它起的名字--本例是这个。如果我们有一个资源的列表,instance_index就可以识别该列表中的实例。我们将在本章后面展示这一点。

与Terraform状态交互

一个有用的命令是terraform console,它使你能够与当前状态互动。你可以执行函数(后面会有更多介绍),你也可以显示资源的一部分。这对于确定某个特定属性的确切语法非常有用。

例如,假设你想输出你配置的服务器的公网IP地址。在console中输入tencentcloud_instance.this然后回车,可以列出该资源的所有属性。你可以看到安全组列表被列在orderly_security_groups中,但你不知道只列出安全组所需的确切语法。

使用console,你可以逐一检查每个资源的属性,并以交互方式遍历该资源的数据结构。你可以看到tencentcloud_instance.this.orderly_security_groups实际上是一个列表(后面会详细介绍),因为一个实例可以有多个安全组。

了解破坏性的变更

一旦你开始使用Terraform并多次运行它,了解Terraform的工作方式至关重要。

使用腾讯云控制台,你应该看到一个名称为state-file的服务器。给这个实例添加一个标签,如图2.1所示。确保你先前往标签管理中创建标签,然后选择刚才创建的标签,以实际应用该标签:

image-20230624123347170

图2.1 - 使用腾讯云控制台添加标签

现在,运行terraform plan。在计划中,Terraform会告知你它将变更一个资源。看一下详细的计划,Terraform显示它将改变标签属性,把你刚刚添加的标签改为null:

 

现在,运行terraform apply,并使用腾讯云控制台检查服务器实例。你会看到,标签已经消失了。Terraform改变了服务器的实际状态,它将一个标签变成了所需的状态,即没有标签。但其他方面没有改变。

留意实例的公网IP地址,现在让我们做另一个变更。这一次,更改Terraform代码,删除启动脚本,编辑main.tf文件,将user_data一行注释掉:

main.tf

 

当你运行terraform plan时,该计划报告它将破坏一个资源并增加另一个资源。如果你看一下细节,你会发现该计划说tencentcloud_instance.this必须被替换。这就是所谓的破坏性变更。

运行terraform apply来执行这个变更。你会看到Terraform首先销毁了这个实例,然后创建了一台新的实例。请注意,实例的公网IP地址已经改变。因此,破坏性的变化可能会产生重大的后果。

我们需要一些时间来了解哪些变更可以就地执行——也就是说,哪些变更不是破坏性的,哪些是破坏性的。因此,你应该经常查看Terraform计划,仔细检查Terraform计划采取的行动,以使实际状态达到预期状态。

避免配置漂移

这给我们带来了基础设施即代码(IaC)的一个重要问题,也就是所谓的配置漂移(有时也被称为环境漂移)。当资源在IaC工具之外(即在Terraform之外)被改变、创建或删除时,就会发生配置漂移。

使用腾讯云控制台或命令行工具(CLI)进行修改是很诱人的,因为往往它更快。然而,这也会产生问题。最好的情况是,在你下次运行Terraform的时候修改会被撤销。在最坏的情况下,一个严重的配置漂移可能需要很多工作来解决。

使用腾讯云控制台或者命令行工具tccli,在广州四区创建一个新的服务器实例,名称为state-file。

如果你是首次使用tccli,需运行tccli configure配置API密钥和默认地域,可以使用第一章创建的API密钥,详情请参见TCCLI配置方法:https://cloud.tencent.com/document/product/440/34012

使用tccli命令创建服务器实例,命令如下:

 

接下来,运行Terraform,但让我们在命令行中使用-var参数给服务器命名为instance-1。它应该看起来像这样:

 

Terraform将销毁一个资源并添加一个新的资源。如下所示,需要替换实例的原因是,服务器的地域从ap-guangzhou-3变成了ap-guangzhou-4:

 

现在,让我们看看当我们应用这个计划时会发生什么(确保指定刚才生成的计划)。命令如下:

 

首先Terraform按照预期删除了位于广州三区,名为state-file的服务器实例。随后在广州四区创建了一台名为state-file的服务器实例。

这说明了一个问题。Terraform只能检查它已知存在的资源,也就是状态文件中的资源。Terraform不知道广州四区有一个名为state-file的服务器存在,所以它继续采取行动来创建它。

Terraform实际上是对腾讯云OpenAPI进行调用。因此,当Terraform试图在广州四区创建一台名为state-file的服务器时仍然会成功。于是广州四区有了两台名为state-file的服务器。

因此,在计划期间,Terraform只知道通过Terraform创建的资源。在本书的后面,你将学习如何将现有资源导入Terraform状态,但我应该提醒你,导入现有资源也存在一些问题。

让我们来继续查看Terraform状态:

 

它给我们返回一个状态中记录的资源列表。现在,我们可以使用以下命令从状态文件中删除资源:

 

你必须了解的是,虽然资源被从状态文件中删除,但它实际上并没有从腾讯云中删除。到腾讯云控制台去看,状态文件中被删除的这台服务器仍然存在。

现在,如果我们运行terraform plan和terraform apply,将产生与之前相同的结果。Terraform将再次创建一台新的服务器。

因此,有两个重要的教训。首先,除非在特殊情况下,否则你不应该在状态文件中添加或删除资源。第二,一旦你使用Terraform(或任何其他IaC工具),你不应该使用腾讯云控制台或CLI来进行修改。所有的改变必须通过Terraform的配置文件来完成。

其他状态命令

现在,有时你想强迫Terraform替换一个资源。terraform taint ADDRESS命令将给资源打上污点——也就是说,它给它贴上标签,以便在下次运行Terraform时,该资源将被移除并重建。

前往腾讯控制台,销毁所有名为state-file的服务器实例。然后,运行terraform apply来重新创建服务器,并将其置于Terraform状态中:

 

当你运行terraform plan时,Terraform没有检测到任何变化。现在,执行以下命令:

 

如果你现在运行terraform plan,Terraform报告说它将替换这个实例。使用terraform untaint命令可以删除污点。

偶尔,Terraform会通知你腾讯云资源元数据的更改,这些更改可能会导致你在接下来生成计划或执行apply时发生更改。因此,偶尔运行terraform refresh是个好主意,它可以用最新的元数据更新当前状态。

使用状态后端

本节的代码在本书的GitHub repo中的chap02/backend下。

默认情况下,Terraform状态被存储在本地的当前目录下,文件名为terraform.tfstate。只要你是唯一在Terraform代码上工作的人,而且你总是在同一台机器上工作,这就没有问题。然而,在大多数情况下,你是团队的一部分,你们在云基础设施上共同工作。如果每个人都存储自己版本的状态文件,就会出现混乱。

为了实现协作,Terraform允许使用状态后端概念来远程存储状态文件。(请注意,这与远程状态不同,我们将在第五章用Terraform管理环境中讨论)。对于腾讯云,远程后端存储在腾讯云存储中。因此,状态文件不是存储在本地,而是存储在云存储桶中,所有的团队成员(有合适的权限)都可以访问该状态文件。此外,后端支持锁定,因此每次只有一个成员可以运行Terraform。当运行Terraform应用时,Terraform会在云存储中放置一个锁,只有在完成所有操作后才释放该锁。如果另一个人或进程试图在锁定状态下运行Terraform,Terraform会报告错误,不执行变更。

要指定一个后端,创建一个名为backend.tf的文件(正如我们提到的,Terraform中的命名在很大程度上是不相关的,但按照惯例,后端是在一个名为backend.tf的文件中指定的):

chap02/backend/backend.tf

 

该存储桶必须事先存在。所以,这是你应该在Terraform之外使用腾讯云控制台创建资源的少数情况之一。存储桶不应该是当前状态文件的一部分,我们只需要为每个腾讯云账号创建一次,

现在,我们前往腾讯云控制台,对象存储,存储桶列表,点击创建存储桶

image-20230625013650924

点击下一步。开启版本管理也是一个好主意,这样你就可以在状态文件出错的情况下恢复之前的版本。再次点击下一步,确认存储桶配置,点击创建。

回到backend.tf,根据刚才创建的存储桶名称,地域,修改bucketregion属性的值。prefix属性的作用是将状态文件存在桶的一个文件夹里。

如果你之前在本地应用了任何基础设施的变化,它们会被存储在本地状态文件中。因此,在你指定后端后,Terraform第一次会询问你是否要将现有状态从本地目录迁移到后端。现在,运行terraform apply

当Terraform运行时,使用腾讯云控制台进入COS对象存储,观察你存储桶中的文件。你会看到一个名为terraform.tfstate.tflock的文件出现,一旦Terraform完成运行就会消失。(请注意.tflock文件只在Terraform运行时存在,所以它可能会很快消失):

image-20230625015147459

图2.2 - Terraform后端状态

现在你对Terraform状态以及何时应该使用状态后端有了很好的理解,让我们转到Terraform的其他概念。

了解Terraform元参数

元参数是Terraform中的一个专用结构体。你已经遇到了Provider元参数,但让我们仔细看看它和其他元参数。

Provider元参数

这一部分的代码在本书的GitHub repo中的chap02/provider下。

我们在第一章中介绍了Provider元参数。从技术上讲,Provider是一个与外部API进行通信的插件——在我们的例子中,Provider是腾讯云OpenAPI。

你可以在你的代码中使用两个相同Provider。假设这样一个场景,你需要在另一个地域再部署一台staging服务器,只需要再次定义一个腾讯云Provider块 (Block) ,添加alias属性并设置一个别名,然后在资源上使用provider属性指定使用哪个Provider,像这样:

chap02/provider/provider.tf

 

chap02/provider/main.tf

 

此外,你可以在你的配置中拥有其他Provider。例如,Terraform有一个Kubernetes的Provider,它提供了一个替代kubectl的工具,kubectl是与Kubernetes交互的默认工具。因此,你可以在代码中引入kubernetes Provider,然后使用Terraform来配置Kubernetes集群中的资源。

建议你只组合有相关性的Provider。一般来说,除非你有充分的理由,否则尽量不要在同一代码中混搭不相关的Provider。

正如我们所提到的,Terraform和它的Provider有一个非常活跃的开发者社区,也就是说,新的版本不断被发布。因此,在Terraform和Provider上设置版本约束是很好的做法。这样做的地方是Terraform块https://developer.hashicorp.com/terraform/language/settings,它通常被插入到provider.tf文件中。required_version设置了对Terraform的版本约束,而required_providers块则指定了Provider的来源和版本约束,如以下代码片段所示:

chap02/provider/provider.tf

 

虽然不是必要的,但包括Terraform块 (Block) 和版本约束是很好的做法,我们从现在开始将这样做。

count元参数

本节的代码在本书的GitHub repo中的chap02/count下。

通常,你会发现自己想要创建(几乎)相同的资源--例如,几个Web服务器。你可以多次复制代码,但这将是乏味的,并使代码的维护更加困难。count元参数提供了在单个块中创建相同资源的能力。让我们来看看下面的代码:

chap2/count/main.tf

 

我们使用count元参数来指定我们要创建的三台服务器。让我们看一下instance_name这一行:

为了让每台服务器有一个唯一的名字。我们可以使用count.index结构体来生成一个唯一的名字,它提供了一个从0开始的索引。 在这里,我们还引入了一个新的语法,叫做插值 (Interpolation) 。插值计算${ ... }标记之间给定的表达式,必要时将结果转换为字符串,然后将其插入到最终的字符串中https://developer.hashicorp.com/terraform/language/expressions/strings

因此,当我们在terraforms.tfvars中把server_name变量设置为webserver时,${var.server_name}-${count.index} 会产生以下字符串:

当你想串联字符串以产生唯一的名字时,插值是很有用的。

for_each元参数

本节的代码在本书的GitHub repo中的chap02/for-each下。

虽然count元参数对于创建几乎相同的资源很有帮助,但有时你想创建有更多变化的资源的多个实例。这就是for_each元参数派上用场的地方。for_each元参数将一个映射 (Map) 或一组字符串作为输入,并为每个项目创建一个不同的资源。

让我们用一个实际的场景作为例子来说明for_each的使用。

我们通常会为每个项目规划一个独立的私有网络 (VPC) 而不是使用默认的VPC。为了设置一个新的VPC,你在需要在不同可用区创建几个子网,这些子网具有不同的CIDR(无类别域间路由)范围。

假设你想创建一个新的虚拟私有云(VPC),需要几个子网。你可以创建两个tencentcloud_subnet资源。如果你想添加第三个子网,你必须再添加一个资源。相反,我们可以用子网和所需信息定义一个映射类型的变量。然后我们可以使用for_each元参数来遍历该映射:

chap02/for-each/terraform.tfvars

 

在此之后,我们可以使用一个的资源来定义我们所有的子网,无论子网的数量如何:

chap02/for-each/vpc.tf

 

我们使用for_each元参数以及each.key和each.value地址来引用映射中的值。然后Terraform为映射中的每个条目创建一个唯一的资源。因此,运行terraform apply来创建一个VPC。

现在,你可以用count和列表实现同样的效果。例如,使用count元参数,你可以定义适当的安全组规则,像这样:

chap02/for-each/sg.tf

 

然后,你可以在terraform.tfvars中指定一个防火墙规则列表:

chap02/for-each/terraform.tfvars

 

然而,有一个区别。改变terraform.tfvars中的子网映射和安全组规则列表的顺序。也就是说,改变子网使guangzhou-four排在guangzhou-three前面,改变安全组规则列表,使allow-http规则排在前面。然后运行terraform apply -auto-approve

子网资源保持不变,但防火墙规则被替换。这是因为在列表中,顺序很重要,而在映射中,顺序并不重要。

因此,当资源几乎相同时使用count元参数,而当它们不同时使用for_each。无论哪种情况,都要谨慎使用,因为虽然你确实可以创建非常紧凑的代码,但对于阅读你的代码的其他人来说,它可能会变得难以理解。

depends_on元参数

Terraform非常擅于确定它需要创建资源的顺序。因此,它知道在创建子网络之前必须先创建一个网络,在创建服务器实例之前必须先创建两者。

然而,你可能也会注意到,Terraform是并行创建资源的。也就是说,它不会等一个资源创建完成后再开始新的创建。默认情况下,Terraform并行地运行10个资源操作。

现在,我们可以通过限制并行操作的数量,迫使Terraform一次只提供一个资源。你可以通过使用-parallelism=n参数来做到这一点,如下所示:

 

然而,这并不是很有效,因为它需要更长的时间来配置所有的资源。相反,在一些特定情况下,你可以明确地声明一个依赖关系,以确保Terraform在试图创建其他资源之前等待资源的创建。在我们的例子中,我们可以明确声明,在Terraform创建子网络之前,必须先创建一个网络。

因此,我们可以用depends_on元参数添加一行,该参数将一个资源列表作为参数。也就是说,在开始创建该资源之前,该列表中的资源需要被完全配置好:

chap02/for-each/vpc.tcf

 

因此,Terraform现在明确地等待网络完成后再创建子网络。一般来说,当Provider无法绘制出更合理的依赖关系时,depends_on元参数被认为是一个手动方案。

还有一个我们需要讨论的元参数:lifecycle(生命周期)元参数。

lifecycle元参数

本节的代码在本书的GitHub repo中的chap02/lifecycle下。

当Terraform需要变更一个资源时,它将在原地进行。这就是所谓的非破坏性改变。然而,当就地更改不可能时,Terraform将首先销毁当前的资源,然后在应用更改后重新创建它。使用lifecycle(生命周期)元参数,你可以影响这种行为。

如果create_before_destroy属性设置为"true",Terraform会在销毁资源之前尝试创建一个新资源。我们在第六章部署传统的三层架构中展示了这一点是如何有用的。

你也可以使用lifecycle元参数来防止一个资源被删除。这对于防止意外的数据丢失很有用。如果prevent_destroy属性被设置为”true“,Terraform在试图销毁资源时,会报告一个错误,并且不会销毁该资源。

这个块中的第三个属性ignore_changes也非常有用。假设你的云环境使用三方工具通过标签来存储信息。然后,每当你运行Terraform的时候,它就会执行一个原地更新的操作,并重置标签。当我们引入ignore_changes元参数时,Terraform在应用其生命周期规则时将忽略列表中任何属性的变化 :

chap02/lifecycle/main.tf

 

运行chap02/lifecycle子目录下的代码创建实例。然后到腾讯云控制台给实例添加一个名为environment值为sandbox的标签。现在,当你运行terraform plan时,Terraform会告知你它将删除这个标签。然而,当你有前面代码片段中所示的lifecycle规则时,Terraform会忽略对标签的任何改变,不变更实例。

元参数是Terraform语言的组成部分,所有的云供应商都会使用。

摘要

在这一章中,我们讨论了Terraform如何使用状态来跟踪当前的资源,并决定它需要执行哪些行动来使当前的状态达到预期的状态。对状态的透彻理解是掌握Terraform和预测Terraform将做什么的关键。我们还介绍了后台的概念,你将Terraform状态存储在腾讯云对象存储中,这样团队就可以在互不干扰的情况下进行协作。我们建议你使用状态后端,即使你自己工作。将Terraform状态存储在腾讯云存储桶中可以保证它不会丢失,如果你在桶中启用版本管理,必要时可以检索旧版本。

然后,我们介绍了元参数,这是特定的Terraform结构,可以影响Terraform的行为,帮助更有效地编写Terraform代码。

在下一章中,我们继续探索Terraform,介绍表达式和函数,帮助你更有效率的编写Terraform代码。

 

第三章 编写高效的Terraform代码

虽然Terraform是一种声明式语言,但有时也需要使用函数式结构来编写高效的代码。在本章中,我们将介绍其中的一些结构,并提供一些编写高效Terraform代码的技巧。特别是我们介绍了动态块 (Dynamic Blocks) 和条件表达式 (Conditional Expressions) 来使代码更加灵活。然后,我们展示了如何使用Terraform的内置函数和引用来自Terraform之外创建的资源信息。在本章的最后,我们讨论了如何使用输出值 (Output Values) 对外暴露信息,并提供了一些高效开发Terraform代码的技巧。因此,在本章结束时,你将能够编写更有效的Terraform代码,并提高效率。

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

技术要求

本章没有额外的技术要求。本章的代码可以在https://github.com/netcmcc/terraform-for-tencentcloud-code/tree/main/chap03找到。像之前一样,我们建议你在进入下一小节之前运行terraform destroy删除你创建的所有资源,以避免产生不必要的费用。

Terraform数据类型和值

本节的代码在本书的GitHub repo中的chap03/types下。

在我们进入表达式之前,让我们仔细看看Terraform的数据类型和值。我们在变量声明中使用了Terraform类型。Terraform有以下几种类型:

前三个是不言自明的。列表是用方括号([ and ])表示的数值序列,用逗号分隔。映射是一组键值对,也就是说,键 = 值。一个映射由大括号({ and })括起来,并由逗号分隔。列表和映射可以被嵌套。下面是一些在变量定义文件中定义的常见类型:

chap03/types/terraform.tfvars

 

例如,var.list[1]指的是列表中的第二个元素。映射中的元素用点符号来引用它们的名字--例如 var.map.ap-guangzhou

对象(Object)和映射一样,但对变量设置了约束。在下面的代码段中,我们定义了一个对象变量,包含两个字符串——name和location,以及一个叫做zones的字符串列表。如果没有完整提供这三个元素,那么Terraform会报告一个错误。这在模块中特别有用,我们将在下一章介绍:

chap03/type/variables.tf

 

Terraform有一个特殊的值null,它代表不存在。我们在本章后面使用这个值。

使用Terraform表达式

在Terraform中,使用表达式(expression)来指代配置中的一个值。因此,像abc这样的字符串或 ["orange", "apple", "strawberry"] 这样的列表被认为是一个简单的表达式。然而,有些表达式更复杂,在编写更灵活的Terraform代码时很有帮助。Terraform提供了两个有用的结构来编写更灵活的代码:动态块(dynamic blocks)和条件表达式(conditional expressions) 。

动态块(Dynamic blocks)

本节的代码在本书的GitHub repo中的chap03/dynamic-block下。

本节动手实验请先跳过,因为遇到了腾讯云Provider的一个Bug,腾讯云正在修复中。

在上一章中,我们介绍了使用count和for_each元参数来创建多个实例的能力。这两个构造,实质上都是用一个块来创建多个资源。块中的块被称为嵌套块,一些嵌套块可以被重复使用。例如,在tencent_compute_instance资源中,你可以使用attached_disk块附加多个磁盘。假设我们想创建一个服务器并将多个磁盘附加到该服务器上,我们可以使用一个变量,这样我们就可以保持磁盘大小和磁盘类型,以及附加模式的灵活性。使用变量还可以让我们轻松地改变磁盘的数量。

首先,你用一个映射变量创建多个磁盘:

chap03/dynamic-block/terraform.tfvars

 

然后,我们可以通过重复data_disks块将一个磁盘附加到服务器上,如下所示:

chap03/dynamic-block/main.tf

 

然而,这产生了两个问题。首先,我们将不得不重复data_disks块。第二,更关键的是,我们不能保持磁盘数量的灵活性。如果我们想把两个或四个磁盘附加到一个服务器上,我们将需要根据附加磁盘的数量来改变我们的代码。

为了解决这个问题,Terraform提供了一个动态块的概念https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks。动态块是你能够在一个顶层块中重复嵌套的块。最好是通过修复之前强调的问题来说明这一点:

chap03/dynamic-block/main.tf

 

用关键字dynamic和一个标签来定义一个动态块。标签指定了嵌套块的种类,在我们的例子中是data_disks。你使用for_each结构来迭代这些值,content关键字定义了嵌套块的主体。在content中,你使用data_disk_sizedata_disk_type动态块的标签和value属性来指代项目的值。因此,data_disks.value指的是var.disks变量,而data_disks.value["size"]分别指的是small-disk、medium-disk和large-disk的size属性。

因此,使用动态块可以使你通过只定义一次块来编写多个重复的块。

条件表达式

本节的代码在本书GitHub仓库的chap03/conditional-expression目录下。

另一个可以用来编写高效、灵活的Terraform代码的概念是条件表达式https://developer.hashicorp.com/terraform/language/expressions/conditionals。条件表达式的语法是很直接的:

 

条件表达式在与count元参数结合时很方便,可以决定是否创建一个资源。

假设你想灵活地给我们的服务器分配一个弹性公网IP。我们声明一个名为public_ip的布尔变量。当设置为"true "时,我们创建一个弹性公网IP,并附加到服务器实例上。当设置为false时,我们不创建公网IP。:

之前我们已经接触过使用allocate_public_ip参数来决定是否为实例分配公网IP。这里介绍为实例分配公网IP的另一种方式。

在中国境内,通过域名访问Web服务器需要完成ICP备案,ICP备案时需要提供服务器IP地址,这个IP地址最好是固定的,因为一旦发生变化,就需要变更备案信息,否则定期进行备案信息核查时,就会要求整改。通常建议备案的EIP长期保留,不随服务器释放。同一个地域的EIP,可以分配不同的云资源,CLB负载均衡器,CVM云服务使用。

我们可以使用单独的terraform资源创建EIP并管理EIP分配:

chap03/conditional-expression/main.tf

 

你可以看到,我们使用了两次条件表达式。

首先,我们在tencentcloud_eip资源上,把它和count元参数一起使用。当public_ip变量被设置为false时,表达式被评估为0,并且没有EIP地址被创建。如果设置为true,表达式的值为1,因此有一个静态IP地址被提供。

之后,在tencentcloud_eip_association资源上,我们再次使用一个条件表达式。如果该变量被设置为true,表达式的值为1,将EIP地址分配给服务器实例。如果设置为false,那么我们分配一个空值,Terraform不会创建和分配EIP地址。

通过切换public_ip变量的值来测试它,然后,使用腾讯云控制台,查看虚拟机(VM)的外部IP:

 

条件表达式的另一个常见作用,是用来决定属性的值。

在无需考虑EIP生命周期的情况下,使用allocate_public_ip属性来控制是否为实例分配EIP会更简洁。但是需要考虑internet_max_bandwidth_out的值。在当前的腾讯云Terraform Provider版本下,如果不分配公网IP的情况下,internet_max_bandwidth_out的值必须为0或者为空值null,否则创建实例时就会产生报错。

 

我们使用条件表达式。根据变量public_ip的值来决定,如何设置带宽参数。

我们使用了一个变量public_ip,如果希望在var.public_ip为false时,不指定internet_max_bandwidth_out的值,利用空值null来达到期望的效果,如下面的代码例子所示:

 

现在,如果你运行以下代码,你会看到internet_max_bandwidth_out的值为0:

 

但是,如果你把static_ip变量设置为"true",你会看到internet_max_bandwidth_out的值未指定:

 

你经常发现自己处于需要创建或转换表达式的位置,为此,Terraform提供了内置函数。

Terraform函数

Terraform有许多内置函数,你可以利用它们来创建或转换表达式。学习它们的最好方法之一是使用terraform console命令互动地使用Terraform。我们在前面介绍了这个命令,以交互方式调查状态。这里是另一种交互式使用Terraform和学习函数的方法:

 

Terraform函数的一般语法是function_name(argument_1, argument_2, ..., argument_n)。Terraform有各种内置函数,包括字符串、数据、数字和文件系统函数。请参考https://www.terraform.io/language/functions的文档,以获得完整的参考信息。

利用数据源引用现有数据

本节的代码在本书的GitHub repo中的chap03/data-source下。

我们在前面讨论过,你不应该把通过Terraform和通过腾讯云控制台创建的资源混在一起,因为这可能导致配置漂移。然而,在有些情况下,你想要使用腾讯云在Terraform之外创建的或不受您控制的云资源的一些数据中的信息。Terraform提供了数据源(Data Sources),更多细节你可以阅读https://developer.hashicorp.com/terraform/language/data-sources

如果你查看腾讯云Terraform Provider文档https://registry.terraform.io/providers/tencentcloudstack/tencentcloud/latest/docs,你会看到一个你可以通过Terraform来配置的腾讯云服务的列表。在每个服务下,通常会分为资源(Resources)和数据源(Data Sources)两个子目录。资源用于配置和管理腾讯云资源,而数据源让你从现有的腾讯云资源中获取信息。

例如,tencentcloud_instance资源https://registry.terraform.io/providers/tencentcloudstack/tencentcloud/latest/docs/resources/instance可以用来创建一个新的服务器实例,而tencentcloud_instances数据源https://registry.terraform.io/providers/tencentcloudstack/tencentcloud/latest/docs/data-sources/instances可以让你引用Terraform之外创建的现有虚拟机实例的信息。

前往腾讯云控制台,创建一台新的服务器实例,将名字设为instance-1,机型选择最小可购买规格,其余参数使用默认值。

或使用tccli命令创建,命令如下:

 

然后,你可以使用tencentcloud_instances数据源检索该实例的所有信息,如以下代码片段所示。你可以引用该实例的任何属性,就像你在Terraform中创建的实例一样。例如,你可以像之前那样输出该实例的IP地址:

chap03/datasource/data.tf

 

一些数据源让你可以获取腾讯云的信息。比方说,你想创建几个服务器,并将它们均匀地分布在一个地域的多个可用区。

你的第一反应可能是使用区域名称和一个数字,因为这是腾讯区域的命名惯例--例如ap-guangzhou-3。然而,虽然大多数腾讯云地域有三个区域,但也有一些地域有四个区域及以上。此外,不同的云产品,对外售卖的地域及可用区存在差异。例如:截止本章写作时,云服务器在广州区域的3、4、5、6可用区售卖,但在上海区域是2、3、4、5、8可用区售卖。

为此,你可以使用带product参数的tencentcloud_availability_zones_by_product数据源。这个数据源以列表形式检索当前区域内提供云服务器售卖的所有可用区。因此,数据源一旦被实例化,你就可以使用name属性来引用zone,如以下的代码片段所示:

chap03/datasource/main.tf

 

通过针对不同区域运行terraform plan来尝试一下。这是一个例子:

 

我们在下面的章节中会介绍更多的数据源。理解数据源和资源之间的区别是至关重要的,因为它们经常共享相同的名称。资源创建新的资源,而数据源则暴露现有资源的信息。然而,使用数据源并不能将现有资源导入Terraform状态,也不能使用数据源管理资源,你只能使用数据源从现有资源中提取信息。

使用输出值

这一部分的代码在本书的GitHub repo中的chap03/output下。

我们简单地使用了输出值,但还没有详细讨论。输出值用于暴露Terraform的信息。到目前为止,我们已经用它们在命令行上输出了IP地址或其他相关的信息。然而,输出值有更多用法,正如我们在下面的章节中看到的。

按照惯例,所有的输出值都放在一个叫做output.tf的文件中。一个输出块需要一个标签参数和一个名为value的参数,它输出的是一个表达式的值。描述参数是可选的,用于描述输出的简短说明。

现在是介绍Splat表达式https://developer.hashicorp.com/terraform/language/expressions/splat的好机会。Splat表达式是for表达式的简称,通过下面的例子可以得到很好的解释。zones-splat和zones-for的value是等价的。Splat表达式在与其他循环结构结合时非常有用--例如:

chap03/output/outputs.tf

 

一旦你运行terraform apply之后只想显示输出,你可以用各种选项运行terraform output。如果没有任何选项,terraform output会以人类可读的形式产生输出:

 

而如果使用-json选项,它将以JSON格式产生:

 

如果你想只显示一个值,你可以指定名称。使用-raw选项不会显示任何引号或换行。这在与其他命令结合时很有用,如下面的例子:

 

输出也被用来在不同的Terraform配置之间暴露信息,正如在下面两章中看到的。

高效开发Terraform代码的技巧

这一部分的代码在本书的GitHub仓库中的chap03/error目录下。

在结束本章之前,我们想介绍两个命令,它们可以帮助你更有效地开发Terraform代码。第一个命令,terraform fmt,格式化Terraform配置文件,使它们遵循一致的格式和缩进。格式化使文件更易读。然而,它也可以作为一个初始检查,因为Terraform会报告一些语法错误。

第二条命令是terraform validate。这个命令执行语法检查,并检查内部一致性。例如,考虑以下文件,它包含一些不正确的Terraform语法:

chap02/error/error.tf

 

首先,运行terraform fmt,它报告了一个错误,指出"}"必须在一个单独的行上。修正错误并重新运行它。没有错误报告,而且文件现在有适当的缩进。现在,运行terraform validate。Terraform将报告一个错误,因为每个资源名称必须是唯一的。尽管terraform plan包括验证步骤,但这个命令很有用,因为它不需要你初始化Terraform,因此,它的执行速度稍快一些。也通常作为CI/CD(持续集成/持续部署)流水线中的一个步骤使用。

在调试Terraform时,设置log level以获得更多的信息可能会很有用。你可以通过将TF_LOG环境变量设置为下列log level之一来实现:TRACE, DEBUG, INFO, WARN, 或者ERROR。更多细节,请访问https://developer.hashicorp.com/terraform/internals/debugging

试一试。在Linux或macOS中,执行以下命令:

 

要把level设置回默认值,可以通过运行以下命令取消设置环境变量:

 

在本书最后一部分,我们将讨论一些有用的第三方工具,这些工具通过执行额外的检查,如静态代码分析,帮助你调试和提高代码的质量。

摘要

本章介绍了几个新的概念,使你能够编写更高效的Terraform代码。动态块是通过定义单个块来编写可重复的嵌套块。虽然Terraform没有明确的if-then概念,但你可以使用条件表达式来有效地编写if-then结构。Terraform提供了一些内置函数来创建和转换表达式。数据源可以引用在Terraform之外定义的资源,有时也会派上用场。最后,我们讨论了输出值,通过它暴露Terraform信息。你将在本书的后面使用这些概念来开发模块,并使用Terraform实现更复杂的部署。

在下一章,我们将介绍Terraform模块。模块允许你复用Terraform的代码块,因此与传统编程语言中的函数有类似的作用。

 

第四章 使用模块编写可复用的代码

基础设施即代码(IaC)的主要目标之一,是使用不要重复自己(DRY)的软件开发原则来创建可复用的代码。Andrew Hunt和David Thomas首先提出了这个原则,从本质上讲,它指出我们应该避免重复代码。

在函数式编程语言中,我们使用函数来保持代码DRY。在Terraform中,我们使用模块。

在本章中,我们将首先概述模块的基本结构,然后学习如何编写灵活的模块,以便对组织中的其他人或社区更容易使用。然后,我们将介绍如何使用私有的和公共的Terraform注册表(Registry)与你的组织或与社区分享代码。

因此,在本章结束时,你将学会如何编写供你以及其他人可以复用的Terraform模块,从而遵循DRY原则。

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

技术要求

本章没有新的技术要求。本章的代码在https://github.com/netcmcc/terraform-for-tencentcloud-code/tree/main/chap04

构建模块

这一部分的代码在本书的GitHub repo中的chap04/local-module目录下。

Terraform中的模块与传统编程语言中的函数作用相同。一个模块是一个独立的代码块,可以重复调用,以创建云基础设施。实际上,我们已经创建了模块。我们编写和执行的Terraform代码被称为根模块(root module)。从其他模块调用并存储在本地的模块,也被称为子模块(child modules) 。因此,根模块可以调用一个子模块,而子模块可以调用其他模块。

一个模块的基本结构很简洁。按照惯例,我们创建三个文件:main.tf,包含主代码;variables.tf,定义需要传递给模块的变量;output.tf,包含传回给调用模块的信息。请记住Terraform本身只关心文件的扩展名,也就是.tf。因此,就Terraform而言,我们可以给这些文件起任何名字,甚至可以把它们合并在一个文件中。然而,惯例是使用这三个文件和命名。

所以,让我们来创建一个提供服务器的模块作为例子。我们首先定义要传递给模块的变量,如以下代码所示:

chap04/local-module/modules/server/variables.tf

 

然后我们定义要提供的资源。请注意,我们可以在一个子模块中定义多个资源,以满足更复杂的业务场景。在我们的例子中,我们想提供一台绑定了弹性公网IP地址的服务器:

chap04/local-module/modules/server/main.tf

 

现在,让我们详细检查一下这段代码。首先,除了有一个数据源用于获取镜像ID外,我们有三个资源块,一个用于创建弹性公网IP地址,一个用于创建实际的服务器,还有一个用于将弹性公网IP绑定到服务器。我们现在看到使用count与条件表达式的技巧是多么有用。如果var.public_ip被设置为true,资源块就会被应用。如果该变量为false,count值为0,则不应用该资源块。

创建服务器实例的资源块除了启动脚本这一行之外,应该看起来很熟悉。我们需要使用path.module变量,它返回模块所在的文件系统路径。如果我们像以前一样使用相对路径,使用./,Terraform将评估路径到当前运行的路径,也就是根模块的位置。

第三个文件是output.tf文件。记得我们说过,输出值的目的是为了暴露信息。当放在根模块中时,输出值的目的通常是向用户传递信息。当在子模块中使用时,输出值的目的是将信息反馈给调用模块。

虽然以前我们不太注意输出值的标签,但在模块中,输出标签是很关键的,因为这是模块的输出被引用的名称。此外,只有定义为输出值的信息才能在调用模块中被引用。在我们的案例中,我们希望同时暴露私有和公网IP地址(如果存在的话):

chap04/local-module/modules/server/outputs.tf

 

一旦我们定义了我们的模块并把它放在一个叫做modules/server的子目录中,我们就可以从根模块中调用它:

chap04/local-module/main.tf

 

在这里,我们正在创建三台服务器,其变化略有不同。我们通过使用模块(Module)和一个标签来调用一个模块。source属性表示模块的位置。如果它指向一个本地目录,该模块就被称为本地模块。接下来,我们将指定模块所期望的参数。source参数属性是强制的。

所有三个服务器都被放置在我们在terraform.vars中定义的可用区。我们希望第一台服务器有一个弹性公网IP地址和我们定义的机型作为默认值。第二台服务器应该与第一台类似,只是它没有弹性公网IP地址。对于第三台服务器,我们明确指定机型。

修订到此处,此标记以下修订中

编写灵活的模块

这一部分的代码在本书的GitHub repo中的chap04/flexible-module目录下。

写得好的模块应该提供一个抽象层,并易于使用。最好是避免简单地编写一个模块来为现有的资源定义提供一个薄薄的包装。模块应该提供额外的功能和抽象性。

例如,除非你是一个有经验的腾讯云架构师,否则诸如C5、M6和SA3等机器类型没有太大意义。对于普通用户来说,为服务器提供尺寸会好得多,例如小型、中型和大型,并让模块决定使用哪种机型。

利用我们之前的例子,我们可以在子模块的变量声明块中指定默认变量值。任何有默认值的变量对调用模块来说都是可选的。我们为模块中除了名字以外的所有变量声明默认值;因此,除了名字以外的所有变量都成为可选。

我们还引入了一个新的machine_size变量,它可以取small、medium、large三个值。然而,我们要确保传递的是一个正确的值。如果调用的模块提供了一个无效的值,我们要标记它并提供一个适当的错误信息。为此,Terraform引入了自定义验证规则,你可以在https://developer.hashicorp.com/terraform/language/values/variables#custom-validation-rules 了解更多信息。自定义验证是一个嵌套在变量声明中的块,具有条件和错误信息属性。如果条件为真,那么变量的值是有效的;否则,它是无效的,并会产生一个错误信息。

让我们把它付诸行动。首先,我们修改变量声明:

chap04/flexible-module/modules/variables.tf

 

然后,我们在模块中增加了main.tf文件。我们引入了一个新的结构,局部值,了解更多关于局部值的信息:https://developer.hashicorp.com/terraform/language/values/locals#local-values

从本质上讲,局部值是可以在一个模块中使用的变量,但不能在模块之外使用。你在locals块中声明局部值。该块包含了局部值的名称和它的值。在我们的例子中,我们使用了两个局部值,machine_type_mapping来定义映射和machine_type,它评估了实际的映射关系:

chap04/flexible-module/modules/main.tf

 

一旦我们对模块进行了修改,我们就可以简化对子模块的调用:

chap04/flexible-module/main.tf

 

我们只需要为server1写两行,即模块的source位置和唯一需要的属性,即服务器名称。我们指定第二台服务器应该是中等尺寸的,模块将规模映射到机器实例类型。我们指定第三台服务器应该是大尺寸的,但没有分配弹性(也没有公网)IP地址。

在我们继续之前,让我们看一下每个服务器被放置的可用区。看一下这些可用区,你可以注意到每个服务器都被放置在不同的可用区。我们没有为第一台服务器指定一个可用区,所以它采用了子模块声明中的默认值,即ap-guangzhou-3。在第二台服务器中,我们使用了根模块中声明的zone变量的值,我们在terraform.tvars文件中定义了这个值为ap-guangzhou-6。对于第三台服务器,我们明确地将该值定义为ap-guangzhou-4。

在本节中,我们描述了如何使模块更灵活、更容易使用,以便我们组织中的其他人能够使用它们。现在,让我们看看如何分享模块。

使用Git存储库共享模块

这一部分的代码在本书的GitHub repo中的chap04/local-module目录下。

包含在我们代码中的模块被称为本地模块。如果我们想与其他项目或其他人分享一个模块,我们可以将模块存储在不同的地方,例如Git仓库。

要使用存储在Git仓库中的模块,我们将使用git::的前缀。Terraform支持GitHub和普通Git仓库。使用Git仓库的好处是它支持版本管理。要指定一个特定的Git版本,你需要附加ref参数,如下面的例子:

 

有关Terraform支持模块的来源类型完整列表,请访问https://developer.hashicorp.com/terraform/language/modules/sources

使用公共模块库

这一部分的代码在本书的GitHub repo中的chap04/registry目录下。

本节例子尚未完成……

对于腾讯云,主要的公共模块库在Terraform注册表https://registry.terraform.io/namespaces/terraform-tencentcloud-modules,社区支持的腾讯云模块在GitHub仓库https://github.com/terraform-tencentcloud-modules

请注意,两个资料库之间有一些重叠。

虽然Terraform注册表和社区模块的源代码都存储在GitHub的公共仓库中,但Terraform注册表提供了一些额外的功能。Terraform注册表的接口使我们很容易发现符合我们需求的模块。此外,Terraform注册表支持版本管理。也就是说,我们可以在调用一个模块时放一个版本约束,以确保我们的代码使用我们测试过的特定版本。例如,在下面的代码中,我们用5.2.0版本测试了我们的代码,并希望确保我们使用这个版本,即使有新的模块版本可用:

chap04/registry/main.tf

 

复杂的模块通常包含子模块,直接调用这些子模块会有好处。Terraform使用一种特殊的语法//来指代模块中的子模块。例如,在前面的配置文件中,我们使用防火墙规则使子模块来配置一个防火墙,使用的方法如下:

 

公共模块可以在两个方面发挥作用。它们包含各种常见的用例,从设置VPC(在这里详细了解- https://github.com/terraform-tencent-modules/terraform-tencent-network/tree/master/modules/vpc)到部署复杂的架构。因此,我们可以利用它们,通过依靠别人的作品来快速配置基础设施。并且,它们也便于向更有经验的Terraform开发者学习。由于所有的代码都可以在公共的GitHub仓库中找到,我们可以看看实际的Terraform代码,来向更有经验的terraform开发者学习。

摘要

在本章中,我们介绍了Terraform的模块。模块是IaC的核心,因为它们是快速有效地重复使用代码的关键部分。模块的结构与我们编写Terraform代码时使用的结构相同。我们在variables.tf中声明变量,将我们的主代码放在main.tf中,并使用output.tf文件将信息暴露给根模块。

然而,明智地使用模块是很重要的。一个模块不应该是现有资源定义的一个精简的包装。一个精心设计的模块通过提供一个抽象层来增加价值。这可以包括在一个模块中配置多个资源,并增加组织模式和政策。在我们这个简单的例子中,我们不仅提供了一个服务器,而且还提供了一个静态的IP地址,我们通过提供简单的服务器大小与实例类型的映射来增加抽象层。

当你开始编写Terraform代码时,总是要考虑到模块。本地模块有助于保持你的代码干燥,特别是在使用多个环境的时候,我们将在下一章看到。

最后,利用公共Terraform模块库。无论你是直接使用还是作为开发自己代码的指南,它们都提供了一个很好的起点。

我们在本书开始时强调,除了复用之外,IaC的一个主要目标是能够提供多个一致的环境,现在我们将在下一章展示这一点。

 

第五章 管理多环境

每个软件的开发都需要多种环境。一般来说,我们至少会使用三个环境,一个用于开发,一个用于测试,一个用于生产。然而,更大、更复杂的项目可能需要额外的环境,如用户验收测试(User Acceptance Test)和预发布(Staging)环境。在传统的企业机房中支持多个环境,其创建和维护成本很高。云计算的魅力在于,我们使用资源几乎是无限的,并且支持弹性计费模式。通过高效的IaC,我们可以快速部署环境,并在不再需要时将其销毁,我们可以根据需要拥有多套环境,但只在实际使用时产生费用。在本章中,我们将看到如何在Terraform中管理多个环境。

本章介绍了Terraform中管理多个环境的两种主要方法——工作区(Workspace)和目录结构。我们将涵盖以下主题:

技术要求

为了遵循这个项目的代码,我们需要使用两个腾讯云区域,一个作为开发,另一个作为生产。我们可以使用我们现有的区域作为开发,为生产环境部署在第二个区域。

本章的代码可以在https://github.com/netcmcc/terraform-for-tencentcloud-code/tree/main/chap05找到。

云资源层次结构

在讨论如何在Terraform中管理环境之前,让我们简单介绍一下云资源的层次结构。在腾讯云中,地域(Region)是指物理数据中心的地理区域。不同地域之间完全隔离,保证不同地域间最大程度的稳定性和容错性。不同地域的云资源价格也有差异,对于网络延时不敏感的业务,或者用于开发测试,可以选择成本更低的地域创建云资源,由于这些地域是完全独立的,所以很合适用来隔离测试和生产环境。

使用独立的地域也能实现命名空间的严格分离。我们可以在开发和生产中使用相同的服务器名称,因为它们驻留在不同的地域中。

例如,我们可以在两个不同地域为开发和生产环境配置独立的资源,如VPC和虚拟机。这些资源甚至可以共享相同的名称。然后,我们可以基于地域设置适当的CAM权限,允许开发团队访问A地域中的开发环境,运维团队访问B地域的生产环境。然而,我们也可以同时授予两个地域的权限,建立资源层次结构提供了一个理想的方式来分离不同的环境。

现代软件工程实践规定,除了微小的配置差异,不同的环境应该是相同的或几乎相同的。这种做法被称为dev/prod parity https://12factor.net/dev-prod-parity。这是为了确保我们在开发环境中开发的应用程序代码在生产环境中也能运行。IaC使我们不仅能够一致地创建这些环境,而且能够在需要的时候迅速提供这些环境,然后在不需要的时候销毁这些环境,以节省成本。

在Terraform中,有两种既定的方法来支持环境--工作区和目录结构。

使用工作区(Workspace)来管理环境

这一部分的代码在本书的GitHub repo中的chap05/workspaces目录下。

工作区允许一份Terraform代码拥有多个独立的状态文件。因此,我们可以为我们的开发环境拥有一个状态文件,为生产环境拥有另一个状态文件。就其本身而言,这可能显得非常有限。然而,结合对变量定义文件(.tfvars)的有效使用,这可以成为管理多个环境的有效方法。

让我们看看如何完成这个任务。使用chap05/workspaces中的示例代码,像平常一样运行terraform initterraform apply。然后,用terraform state list看一下状态文件:

 

我们可以在状态文件中看到这三个服务器和它们的公网IP地址。

接下来,使用terraform workspace new prod命令创建一个名为prod的新工作区。terraform workspace list命令显示了所有当前的工作区,并确认我们现在是在prod工作区。

如果我们再次运行terraform state list,Terraform将返回一个空列表。因为刚才Terraform创建了一个新的状态文件,所以状态是空的:

 

现在,运行terraform apply -var region=ap-shanghai,其中<PROJECT_ID>是我们第二个地域的ID,即我们的生产地域。在命令行中指定region会覆盖terraform.tfvars中提供的值。我们可以看到,它在广州和上海地域中创建了相同的资源。

看一下以下命令:

 

现在,使用腾讯云控制台,在云服务器实例列表中切换不同地域。我们可以看到,我们已经创建了两个相同的环境,每个地域都有一个。

继续在腾讯云控制台,进入对象存储查看状态后端的存储桶,在chap05/workspaces目录中。我们可以看到两个.tfstate文件,default.tfstate和prod.tfstate。因此,Terraform现在管理着两个独立版本的状态文件。你可以使用terraform workspace list命令在两个工作区之间切换,并使用不同的状态文件。

一般来说,通过命令行传递变量不是很有效。因此,最好创建第二个变量定义文件(.tfvars),并在命令行中传递。也就是说,我们编辑prod.tfvars文件,添加我们生产环境的地域ID,然后运行terraform apply -var-file prod.tfvars

将工作区与地域相结合,可以成为管理并隔离不同环境中几乎相同的部署的有利工具,但这种方法有一些局限性。因此,许多组织使用目录结构来管理不同的环境。

使用目录结构来管理环境

这一部分的代码在本书的GitHub repo中的chap05/directory-structure目录下。

我们看到,使用工作区和地域是一种管理几乎相同环境的简单方法。我们所要做的就是创建一个额外的工作区并在我们的变量定义文件中提供不同的值。然而,这种方法的一个主要限制是,环境必须是几乎相同的。

用我们的例子来说,假设我们想在开发环境中拥有两个小型服务器,但在生产环境中,我们想拥有两个中型和一个大型服务器。配置两个不同大小的服务器很容易,因为我们可以在变量定义文件中指定不同的服务器大小。然而,只在生产环境中拥有第三台不同配置的服务器则更具挑战性。我们可以定义一个条件表达式和计数元参数的组合,如下所示:

 

然而,这可能会很快变得过于复杂。一个更好的方法是为每个环境设置不同的子目录,并使用模块来保持我们的代码DRY。所以,如果我们想有两个环境,我们的目录结构看起来像下面这样:

 

我们有三个子目录,dev、prod和modules。dev和prod都包含常规的文件,包括它们自己的状态后端和变量定义文件。modules子目录像以前一样包含服务器模块的Terraform代码文件。

因此,看一下两个main.tf文件,我们可以看到我们如何为dev定义两个小服务器,为prod定义三个不同大小的服务器:

chap05/directory-structure/dev/main.tf

 

在prod/main.tf中,我们改变了一些变量设置,并为第三个服务器增加了一个额外的模块声明:

chap05/directory-structure/prod/main.tf

 

我们使用了数据源来获取当前地域售卖云服务器的可用区,然后使用索引为服务器指定不同可用区ID,这样即使在不同地域也可以正确设置可用区ID。

有了这些文件,我们现在可以在每个子目录下运行Terraform,指定相应的区域作为参数:

 

这种结构允许我们在不同的环境中拥有不同的配置定义,但又能保持我们的代码DRY。这种方法需要有效地使用模块,无论它们是存储在本地还是远程的Git仓库或注册表中。当使用独立的目录时,我们经常需要在不同子目录的配置之间共享状态数据。为此,Terraform提供了远程状态的概念。

使用远程状态

这一部分的代码在本书的GitHub repo中的chap05/remote-state目录下。

使用目录结构的方法提供了额外的灵活性。在创建复杂的架构时,将复杂的配置分解成更小、更容易管理的部分,通常称为层(Layers),这是一个很好的方法。每一层都是资源的逻辑分组。例如,假设我们要配置一个数据库和一组连接到数据库的服务器。例如,我们有两个团队,一个负责数据库,另一个负责管理服务器。例如,虽然数据库相对稳定,应该一直在运行,但我们预计服务器会被重新创建很多次,我们可能想在周末销毁服务器以节省成本。

我们可以通过创建两个层并将与该层相关的所有代码放在不同的子目录中来实现这一目标。也就是说,我们创建两个子目录,mysql和cvm。每个子目录都有自己的代码定义,包括状态后端和变量定义文件。因此,文件结构看起来如下:

 

让我们首先配置MySQL数据库实例。

chap05/remote-state/mysql/main.tf

 

现在,我们在每个子目录中独立运行Terraform,从MySQL开始,但这产生了一个问题。由于每个子目录都有自己的状态文件,我们如何共享MySQL实例的信息?例如,为了连接到数据库,服务器需要知道Terraform创建的数据库的端点。但是,由于MySQL数据库被配置在不同的子目录下,有自己的状态,所以计算实例目录下的配置不能访问tencentcloud_mysql_instance的任何属性。

现在,我们可以使用tencentcloud_mysql_instance数据源https://registry.terraform.io/providers/tencentcloudstack/tencentcloud/latest/docs/data-sources/mysql_instance,获得数据库的名称,但有一个更好的方法。

terraform_remote_state数据源https://developer.hashicorp.com/terraform/language/state/remote-state-data,使我们能够使用来自单独的Terraform配置的信息。也就是说,在我们的例子中,我们可以使用来自tencentcloud_mysql_instance资源的状态信息,该资源被定义在mysql目录下。如前所述,我们需要定义输出值来暴露我们想要共享的信息。由于我们想分享数据库的endpoint值,我们在output.tf文件中公开了该值:

chap05/remote-state/mysql/outputs.tf

 

为了使用该信息,我们使用cvm目录中的terraform_remote_state数据源来检索该信息。然后,我们可以通过data.terraform_remote_state.mysql.outputs.endpoint表达式来使用endpoint,如下面的data.tf文件所示:

chap05/remote-state/cvm/data.tf

 

现在,假设我们想在我们的启动脚本文件中使用这个连接名称。例如,我们想把它设置为一个环境变量,我们的应用程序代码可以用它来创建一个与数据库的连接。我们怎样才能把这些信息传递给我们的启动脚本呢?

使用模板文件

这一部分的代码在本书的GitHub repo中的chap05/remote-state目录下。

Terraform提供了一个名为templatefile的内置函数,在此了解更多信息:https://developer.hashicorp.com/terraform/language/functions/templatefile

这个函数接收一个使用Terraform表达式的文件并对其进行评估。我们可以在文件中使用变量、资源属性和其他表达式,Terraform评估这些表达式并将其替换为文件中的值。让我们在实践中看到这一点。首先,我们定义一个模板文件。按照惯例,模板文件使用*.tftpl扩展名:

chap05/cvm/startup.tftpl

 

在运行期间,Terraform评估data.terraform_remote_state.mysql.outputs.endpoint表达式并将其替换为值。

规模化管理基础设施

在结束本章之前,让我们看一下状态文件的另一个方面。到目前为止,我们的Terraform文件是可以管理的,因为我们只同时部署了几个资源。然而,即使是一个中等复杂度的架构也需要几十或几百个相互依赖的资源。通过使用模块,我们可以减少Terraform代码的这种复杂性,但我们仍将提供许多具有依赖性的云资源。此外,随着我们对Terraform使用的增加,我们将由几个团队成员同时开发Terraform代码。那么,在一个状态文件中管理所有的资源就变得很有挑战性。例如,负责网络的团队成员可能只是想在另一个团队成员运行Terraform添加虚拟机时进行更改,因此被阻止同时运行Terraform。或者,数据库团队想使用最新Provider版本中的一个新功能,而网络团队还没来得及测试。

因此,将状态文件分解成可以独立管理的独立部分是有意义的。当使用目录结构时,每个子目录都有自己的状态文件。拥有多个状态文件会带来几个好处。首先,我们可以使用状态文件来分离所有权。例如,通过将所有网络和数据库相关的资源划分为不同的目录,每个目录都可以有其独立的所有权和状态文件。然后我们可以把这些资源的责任分别分配给网络和数据库团队。

第二,我们限制了爆炸的半径。在较小的计划中,与所有东西都在同一个目录和一个状态文件中的情况相比,我们不太可能错过破坏性的变化。

此外,提供分离允许更细化和更安全的升级路径。正如我们所提到的,Terraform是一个非常动态的环境,Terraform和供应商都会进行频繁的更新。通过将状态文件分离成独立的单元,每个部分都可以自行管理升级。

摘要

在本章中,我们介绍了Terraform中支持多个环境的两种常用方法,即工作区和目录结构。工作区创建独立的状态文件,我们可以用它来管理多个环境——虽然对于简单的部署来说已经足够了,但在腾讯云中,目录是一种更灵活的分离环境的方式,一般用于管理环境。

现在我们已经用简单的例子介绍了Terraform的基本原理,让我们用你目前学到的技术建立更复杂的部署。我们首先建立一个传统的三层架构。然后,我们将使用公共Terraform资源库来快速配置TKE集群,再了解如何建立现代无服务器架构。