跳到主要内容

4.terraform使用案例-创建本地文件

什么叫基础设施即代码?

在以前,当我们需要把应用部署在服务器时,需要购买多台服务器和机房、组装交换机和网络、不间断电源UPS等。随着云时代的到来,我们可以在IaaS(Infrastructure as a Service)平台直接购买所有的基础设施,包括服务器、专用网络、DNS、负载均衡等,而你只需要专注于应用层面即可。

IaaS(Infrastructure as a Service)的意思是基础设施即服务,它是云服务的基础。著名的IaaS厂商有亚马逊、微软、谷歌和阿里云等。

云厂商为我们解决了许多运维问题:我们不再需要自己管理物理机器,而且能够根据需要随时创建和销毁云机器,还能根据业务和性能要求指定创建服务器的配置和数量。这种便利对于创业型的小公司和个人开发者尤其重要。

随时公司业务的良好发展,所需要的硬件资源越来越多,架构越来越复杂。通过界面操作手工创建服务器、数据库等资源的方式带来越来越多的问题。首先,只要是人工操作,都会有失误的可能,没有人能保证自己不会犯错;而人工操作在软件行业发生事故的案例屡见不鲜。其次,为保证正确率,人工操作一般只能串行,资源多的时候时间会很长。最后,如果我需要根据开发环境的配置再创建一个测试环境和生产环境,人工操作可能会造成差异和错误。

因此,对于这种复杂需要,最佳的方式是通过代码来创建所有硬件资源。这种思想就是基础设施即代码(Infrastructure as Code,很简称IaC),通过代码与定义、部署、更新和销毁基础设施。把硬件映射为软件,而开发和运维人员通过管理代码来管理硬件,terraform 是这一领域的事实标准。

IaC的好处有:

  • 自动化:与软件代替人工,实现自动化,减少风险和安全问题;
  • 效率高:软件可以并行创建资源,大大提高效率;
  • 记录与追踪:通过代码与执行情况,记录硬件变更,出问题也可以追溯;
  • 重用与复制:抽取公共模块实现重用,如创建一个Kubernetes集群的资源可以封装成一个模块。

terraform 介绍

Terraform 是由HashiCorp公司研发的开源的IaC工具,它是由GO语言编写的,可以在各个平台上运行,支持Linux、Mac、Windows等。它简单易用,即使没有太多代码经验的人,也能读懂Terraform的配置代码HCL。

Terraform是一个由Go语言编写的程序,它会读取HCL语言编写的配置文件,然后将变更信息通过RPC与插件通信,由插件调用云厂商的API完成变更操作。这就是Terraform的工作原理,架构图如下:

创建一个文件

Terraform的主要应用场景是云服务的基础设施管理,但为了让大家能快速的接触与体验Terraform,我会先选择最简单的一个插件来入门,以免需要太多的环境设置。我们的任务是创建一个文本文件,内容由我们来指定。可以通过插件hashicorp/local来完成。

  1. 在当前目录创建一个main.tf文件,完整的代码如下:
terraform {
required_providers {
tencentcloud = {
source = "hashicorp/local"
version = "2.4.0"
}
}
}

resource "local_file" "foo" {
content = "foo!"
filename = "${path.module}/foo.bar"
}
  1. 执行 terraform init

看命令的输出结果可以知道,Terraform会自动帮我们去下载对应版本的插件hashicorp/local,并做一些初始化的操作。我们从上图可以看到文件夹下多了2个文件

  • .terraform

    用来存放插件,插件默认是从官方的公共仓库registry.terraform.io下载的,你也可以指定其他代码仓库。

  • .terraform.lock.hcl

    Terraform 配置可能会指向外部的两种不同类型的外部依赖项:providers & modules。 这两种依赖关系类型都可以独立于 Terraform 本身和依赖于它们的配置进行发布和更新。因此,Terraform 必须确定这些依赖项的哪些版本可能与当前配置兼容,以及当前选择使用哪些版本。每次运行 terraform init 命令时,Terraform 都会自动创建或更新依赖项锁定文件。应将此文件包含在版本控制存储库中,以便可以通过代码审查讨论对外部依赖项的潜在更改,就像讨论对配置本身的潜在更改一样。依赖关系锁文件使用与主 Terraform 语言相同的低级语法,但依赖关系锁文件本身不是 Terraform 语言配置文件。它以后缀 .hcl 而不是 .tf 命名,以表示这种差异。

  1. 执行 terraform plan 预览变更:

这里表示我们会创建一个文件内容为 “foo!”,权限为0777,文件名为 foo.bar 的文件。

  1. 执行 terraform apply 部署:

成功执行,多了两个文件:

  • foo.bar

    我们想创建的文件,文件内容为 ”foo!“

  • terraform.tfstat

    tfstate 文件,全名为 Terraform State 文件,是 Terraform 用来存储管理的基础设施的当前状态的文件。这个文件对 Terraform 来说非常重要,因为它允许 Terraform 知道当前的资源配置和实际的基础设施之间的映射。

    当你运行 terraform apply 来创建或更新资源时,Terraform 会使用配置文件来确定应该创建或更改哪些资源。然而,为了知道哪些资源已经存在以及它们的当前配置,Terraform 需要一个参考点,这就是 tfstate 文件的作用。 tfstate 文件通常包含以下信息:

    已经创建的资源的信息,包括它们的属性和配置。 用于将配置中的资源与实际基础设施中的资源相映射的元数据。 资源之间的依赖关系。 需要注意的是,tfstate 文件可能包含敏感信息,因此应该妥善处理并存储在安全的位置。

    Terraform 支持本地和远程状态存储。对于团队协作和生产环境,通常推荐使用远程状态存储,例如 Amazon S3、Azure Blob Storage 或 Terraform Cloud,这样可以防止状态不一致和并发修改的问题。

    如果你对状态文件做出更改,或者不小心删除了它,这可能会导致 Terraform 丢失对你的基础设施的追踪。因此,处理 tfstate 文件时要非常小心,并定期创建备份。

    正因为有了 state 文件,我们再次运行 apply,terraform 不会再给我们创建新的文件,因为他知道当前状态。

variables and outputs

  • Input Variables 用作 Terraform 模块的参数,因此用户可以自定义行为而无需编辑源。
  • Output Values 模块返回值
  • Local Values 用于为表达式分配短名称的便捷功能。

Input Variables

输入变量允许您自定义 Terraform 模块的各个方面,而无需更改模块自己的源代码。此功能允许您在不同的 Terraform 配置之间共享模块,从而使您的模块可组合和可重用。

在配置的根模块中声明变量时,可以使用 CLI 选项和环境变量设置其值。在子模块中声明它们时,调用模块应在 module 块中传递值。

Input variables 类似于函数参数。Output values 类似于函数返回值。Local values 类似于函数的临时局部变量。

declaring an input variable

模块接受的每个输入变量都必须使用 variable 块声明:

variable "image_id" {
type = string
}

variable "availability_zone_names" {
type = list(string)
default = ["us-west-1a"]
}

variable "docker_ports" {
type = list(object({
internal = number
external = number
protocol = string
}))
default = [
{
internal = 8300
external = 8300
protocol = "tcp"
}
]
}

variable 关键字后面的标签是变量的名称,该名称在同一模块中的所有变量中必须是唯一的。此名称用于从外部为变量赋值,并从模块内部引用变量的值。

变量声明还可以包含 default 参数。如果存在,则该变量被视为可选变量,如果在调用模块或运行 Terraform 时未设置任何值,则将使用默认值。 default 参数需要文本值,并且不能引用配置中的其他对象。

variable 块中的 type 参数允许您限制将接受为变量值的值的类型。如果未设置类型约束,则接受任何类型的值。

类型约束是由类型关键字和类型构造函数的混合创建的。支持的类型关键字包括:

  • string
  • number
  • bool

类型构造函数允许您指定复杂类型,例如集合:

  • list
  • set
  • map
  • object
  • tuple

由于模块的输入变量是其用户界面的一部分,因此您可以使用可选的 description 参数简要描述每个变量的用途。您也可以通过在相应的 variable 块中添加 validation 块来为特定变量指定自定义验证规则。以下示例检查 AMI ID 是否具有正确的语法。

variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."

validation {
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}

Using Input Variable Values

仅在声明变量的模块中,可以从表达式中以 var.NAME 的形式访问其值,其中 NAME 与声明块中给出的标签匹配:

resource "aws_instance" "example" {
instance_type = "t2.micro"
ami = var.image_id
}

Assigning Values to Root Module Variables

在配置的根模块中声明变量时,可以通过多种方式设置它们:

  • In a Terraform Cloud workspace.
  • Individually, with the -var command line option.
  • In variable definitions (.tfvars) files, either specified on the command line or automatically loaded.
  • As environment variables.

Variable Definitions (.tfvars) Files

要设置大量变量,更方便的做法是在变量定义文件中指定它们的值(文件名以 .tfvars 或 .tfvars.json 结尾),然后在命令行上使用 -var-file 指定该文件:

terraform apply -var-file="testing.tfvars"

变量定义文件使用与 Terraform 语言文件相同的基本语法,但仅包含变量名称赋值:

image_id = "ami-abc123"
availability_zone_names = [
"us-east-1a",
"us-west-1c",
]

Environment Variables

Terraform 在其自身进程的环境中搜索名为 TFVAR 的环境变量,后跟已声明变量的名称。

$ export TF_VAR_image_id=ami-abc123
$ terraform plan
...

Output Values

输出值有多种用途:

  • 子模块可以使用输出将其资源属性的子集公开给父模块。
  • 根模块可以使用输出在运行 terraform apply 后在 CLI 输出中打印某些值。

Declaring an Output Value

模块导出的每个输出值都必须使用 output 块声明:

output "instance_ip_addr" {
value = aws_instance.server.private_ip
}

紧跟在 output 关键字后面的标签是名称,它必须是有效的标识符。在根模块中,向用户显示此名称;在子模块中,它可用于访问输出的值。

value 参数采用其结果将返回给用户的表达式。在此示例中,表达式引用由此模块中其他地方定义的 aws_instance 资源公开的 private_ip 属性(未显示)。允许任何有效的表达式作为输出值。

Accessing Child Module Outputs

在父模块中,子模块的输出在表达式中以 module.MODULE NAME.OUTPUT NAME 形式提供。例如,如果名为 web_server 的子模块声明了名为 instance_ip_addr 的输出,则可以将该值访问为 module.web_server.instance_ip_addr 。

Modules

模块化是Terraform实现代码重用的方式。模块可以理解为一个包含多个资源的容器模板。封装好之后,可以给大家使用。也可以理解为代码中的函数或方法,它接收入参,经过一些声明式的调用后,输出一些结果变量。

从Terraform的代码层面来看,模块其实就是一个包含多个.tf或.tf.json文件的目录。任何一个Terraform项目,都是一个目录,所以也都是一个模块,我们把它称为根模块(Root Module)。而在它目录下的其它模块,都是子模块。我们可以调用多个模块,也可以多次调用同一个子模块。在子模块中,也可以调用其它模块。这些特点,与函数无异。

调用模块有两种方式,一种是在当前项目定义一个模块,另一种是引入外部的模块。而外部模块的方式也很多种,如Git的仓库、压缩文件等。

modules 有什么用

组织配置 - 模块通过将配置的相关部分放在一起,可以更轻松地导航、理解和更新配置。即使是中等复杂的基础架构也可能需要数百或数千行配置才能实现。通过使用模块,您可以将配置组织到逻辑组件中。

封装配置 - 使用模块的另一个好处是将配置封装到不同的逻辑组件中。封装有助于防止意外后果,例如对配置的一部分的更改意外导致对其他基础结构的更改,并减少简单错误(如对两个不同资源使用相同的名称)的可能性。 重用配置 - 从头开始编写所有配置可能既耗时又容易出错。使用模块可以通过重用由您自己、团队的其他成员或已发布模块供您使用的其他 Terraform 从业者编写的配置来节省时间并减少代价高昂的错误。您还可以与您的团队或公众共享您编写的模块,让他们从您的辛勤工作中受益。

什么是 modules

Terraform 模块是单个目录中的一组 Terraform 配置文件。即使是由具有一个或多个 .tf 文件的单个目录组成的简单配置也是一个模块。直接从此类目录运行 Terraform 命令时,它被视为根模块。因此,从这个意义上说,每个 Terraform 配置都是模块的一部分。您可能有一组简单的 Terraform 配置文件,例如:

.
├── LICENSE
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf

Terraform 命令只会直接使用一个目录中的配置文件,该目录通常是当前工作目录。但是,您的配置可以使用模块块来调用其他目录中的模块。当 Terraform 遇到模块块时,它会加载并处理该模块的配置文件。由另一个配置调用的模块有时称为该配置的“子模块”。

模块可以从本地文件系统或远程源加载。Terraform 支持各种远程源,包括 Terraform Registry、大多数版本控制系统、HTTP URL 以及 Terraform Cloud 或 Terraform Enterprise 私有模块注册表。

最佳实践

在许多方面,Terraform 模块类似于大多数编程语言中的库、包或模块的概念,并提供许多相同的优势。就像几乎所有重要的计算机程序一样,现实世界的 Terraform 配置几乎总是使用模块来提供上述好处。

  1. 定义并使用模块

我们先来使用第一种方式,引用当前项目中的模块。子模块的功能很简单,创建一个文件,文件名有随机字符串,以避免冲突。写入文件的内容可以通过参数指定。定义入参:创建一个文件叫variables.tf,专门用来定义入参:

variable "prefix" {
type = string
default = "pkslow"
description = "File name prefix"
}

variable "content" {
type = string
default = "www.pkslow.com"
description = "File content"
}

这里输入有两个变量,都是字符串类型,分别是文件名前缀prefix和文件内容context。定义模块功能,主要配置这个模块用管理的资源,一般会放在main.tf文件中,内容如下:

resource "random_string" "random" {
length = 6
lower = true
special = false
}

resource "local_file" "file" {
content = var.content
filename = "${path.root}/${var.prefix}.${random_string.random.result}.txt"
}

这里定义了两个resource,第一个是生成6位的随机字符串。第二个是生成一个文件,第二个resource使用了输入参数,还使用了第一个资源生成的结果。所以第二个resource是依赖于第一个的。输入的变量引用方式为var.xxx。

  1. 定义返回值:

可以不需要返回值,也可以定义一个或多个返回值。创建一个outputs.tf文件,内容如下:

output "file_name" { value = local_file.file.filename }

它返回的是前面第二个resource中的值。

现在,模块random-file已经定义完成了。现在我们在根模块调用这个子模块。代码如下:

module "local-file" {
source = "./random-file"
prefix = "pkslow"
content = "Hi guys, this is www.pkslow.com\nBest wishes!"
}

这个source是被调用模块的地址。prefix和content都是入参,之前已经定义了。

在根模块也可以定义输出变量:

output "fileName" {
value = module.local-file.file_name
}
  1. 多个block调用同一个module

我们说过模块是为了实现代码复用,Terraform允许一个模块被多次调用。我们修改根模块的调用代码:

module "pkslow-file" {
source = "./random-file"
prefix = "pkslow"
content = "Hi guys, this is www.pkslow.com\nBest wishes!"
}

module "larry-file" {
source = "./random-file"
prefix = "larrydpk"
content = "Hi guys, this is Larry Deng!"
}

这里两个调用的source都是一样的,都调用了random-file这个模块,只是入参不同。

根模块的输出也修改一下:

output "pkslowPileName" {
value = module.pkslow-file.file_name
}

output "larryFileName" {
value = module.larry-file.file_name
}

执行apply后output输出结果为:

$ terraform output
larryFileName = "./larrydpk.txoV34.txt"
pkslowPileName = "./pkslow.WnJVMm.txt"
  1. 循环调用一个module

多次调用一个模块还有另一种方式就是循环调用,通过count来实现,具体如下:

module "pkslow-file" {
count = 6
source = "./random-file"
prefix = "pkslow-${count.index}"
content = "Hi guys, this is www.pkslow.com\nBest wishes!"
}

这里会调用6次子模块random-file,下标索引为count.index,它是从0开始的索引。

因此,执行后,会生成以下6个文件:

pkslow-0.JBDuhH.txt
pkslow-1.Z6QmPV.txt
pkslow-2.PlCK5u.txt
pkslow-3.a70sWN.txt
pkslow-4.UnxYue.txt
pkslow-5.8bSNxg.txt

这里根模块的输出就需要修改了,它成了一个List,通过*引用所有元素:

output "pkslowPileNameList" {
value = module.pkslow-file.*.file_name
}
  1. 引用外部模块

除了在本项目中定义并引用模块之外,还可以引用外部的模块。在官方的仓库中已经有非常多的可重用的模块了,可以到上面查找:https://registry.terraform.io/browse/modules

比如我引用了( https://registry.terraform.io/modules/matti/resource/shell/latest )这个模块:

module "echo-larry-result" {
source = "matti/resource/shell"
version = "1.5.0"
command = "cat ${module.larry-file.file_name}"
}

执行terraform get会从仓库下载模块:

$ terraform get
Downloading matti/resource/shell 1.5.0 for echo-larry-result...
- echo-larry-result in .terraform/modules/echo-larry-result
- larry-file in random-file
- pkslow-file in random-file

在.modules目录下可以查看模块内容。

这个模块可以执行shell命令,并返回结果。我这里执行的命令是读取之前生成文件的内容。输出调用结果:

output "larryFileResult" {
value = module.echo-larry-result.stdout
}

执行结果如下:

larryFileName = "./.result/larrydpk.GfgMyh.txt"
larryFileResult = "Hi guys, this is Larry Deng!"
  1. 模块来源

引入模块的来源很多:

本地目录

  • Terraform官方仓库
  • GitHub或其它Git仓库
  • Bitbucket
  • HTTP URLs
  • S3 Buckets
  • GCS Bucket

非常方便。我们已经介绍过比较常用的前两种了,其它更多细节可以参考:https://www.terraform.io/docs/language/modules/sources.html

  1. 引用