Terraform介绍

Terraform是HashiCorp公司旗下的Provision Infrastructure产品, 是AWS APN Technology Partner与AWS DevOps Competency Partner。Terraform是一个IT基础架构自动化编排工具,它的口号是“Write, Plan, and Create Infrastructure as Code”, 是一个“基础设施即代码”工具,类似于AWS CloudFormation,允许您创建、更新和版本控制的AWS基础设施。

Terraform基于AWS Go SDK进行构建,采用HashiCorp配置语言(HCL)对资源进行编排,具体的说就是可以用代码来管理维护IT资源,比如针对AWS,我们可以用它创建、修改或删除 S3 Bucket、Lambda,、EC2、Kinesis、VPC等各种资源。并且在真正运行之前可以看到执行计划(即干运行-dryrun)。由于状态保存到文件中,因此能够离线方式查看资源情况(前提是不要在 Terraform 之外对资源进行修改)。Terraform 配置的状态除了能够保存在本地文件中,也可以保存到 Consul, S3等处。

Terraform是一个高度可扩展的工具,通过Provider来扩展对新的基础架构的支持,几乎支持所有的云服务平台,AWS只是Terraform内建 Providers 中的一种,国内还支持Aliyun,HuaweiCloud,TencentCloud。

在Terraform诞生之前,我们对AWS资源的操作主要依赖Console、AWS CLI、SDK或Serverless。AWS CLI什么都能做,但它是无状态的,必须明确用不同的命令来创建、修改和删除。Serverless不是用来管理基础架构的,用Lambda创建资源是很麻烦的事。更通俗的讲,Terraform 就是运行在客户端的一个开源的,用于资源编排的自动化运维工具。以代码的形式将所要管理的资源定义在模板中,通过解析并执行模板来自动化完成所定义资源的创建,变更和管理,进而达到自动化运维的目标。

值得一提的是B站除了自建的idc机房以外还对接国内外30+的公有云平台,目前B站的公有云云管平台底层也计划采用Terraform + 云厂商Openapi的方式进行实现。

Terraform

Terraform特点

Terraform 具备以下几个主要特点:

  • 基础设施即代码(IaC, Infrastructure as Code)
    Terraform 基于一种特定的配置语言(HCL, Hashicorp Configuration Language)来描述基础设施资源。由此,可以像对待任何其他代码一样,实现对所描述的解决方案或者基础架构的版本控制和管理。同时,通用的解决方案和基础架构可以以模板的形式进行便捷的共享和重用。
  • 执行计划(Execution Plans)
    Terraform 在执行模板前,运行 terraform plan 命令会先通过解析模板生成一个可执行的计划,这个计划展示了当前模板所要创建或变更的资源及其属性。操作人员可以预览这个计划,在确认无误后执行 terraform apply 命令,即可完成对所定义资源的快速创建和变更,以免发生一些超预期的问题。
  • 资源拓扑图(Resource Graph)
    Terraform 会根据模板中的定义,构建所有资源的图形,并且以并行的方式创建和修改那些没有任何依赖资源的资源,以保证执行的高效性。对于有依赖资源的资源,被依赖的资源优先执行。
  • 自动化变更(Change Automation)
    不论多复杂的资源,当模板中定义的资源内容发生变更时,Terraform 都会基于新的资源拓扑图将变更的内容plan 出来,在确认无误后,只需一个命令即可完成数个变更操作,避免了人为操作带来的错误。

Terraform环境安装

https://www.terraform.io/downloads

Linux安装

1
2
3
4
5
6
7
8
yum install -y yum-utils
#安装yum扩展工具

yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.rep
#添加hashicorp yum仓库

yum -y install terraform
#安装terraform

Windows安装

1
2
3
4
5
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# 安装choco包管理器

choco install terraform
#通过choco包管理器安装terraform环境

资源创建

这里以AWS云为例,实现一套简单的资源创建:

  1. 创建一个vpc
  2. 创建一个互联网网关
  3. 创建一个路由表并写入默认路由指向第2步创建的互联网网关
  4. 在vpc中创建两个子网(一般要创建两个,否则部分资源无法高可用如RDS)
  5. 将子网和路由表进行关联(否则子网下创建的资源无法访问外网)
  6. 创建一个安全组,并写入入放行规则 放行指定端口(给ec2机器备用)
  7. 在其中一个子网下创建一个网络接口(给ec2机器备用),关联第6步创建的安全组并分配一个EIP
  8. 创建一台aws ec2机器(启用:选择指定的ami即镜像id,实例规格,可用区,密钥对,关联第7步的网络接口)。自定义启动脚本(安装并启动一个nginx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# main.tf

# aws各地域/代称
# US East (Ohio)/us-east-2 # US East (N. Virginia)/us-east-1 # US West (N. California)/us-west-1
# US West (Oregon)/us-west-2 # Africa (Cape Town)/af-south-1 # Asia Pacific (Hong Kong)/ap-east-1
# 亚太地区(孟买)/ap-south-1 # 亚太地区(大阪)/ap-northeast-3 # 亚太地区(首尔)/ap-northeast-2
# 亚太地区(新加坡)/ap-southeast-1 # 亚太地区(悉尼)/ap-southeast-2 # 亚太地区(东京)/ap-northeast-1
# 加拿大(中部)/ca-central-1 # 中国(北京)/cn-north-1 # 中国(宁夏)/cn-northwest-1
# 欧洲(法兰克福)/eu-central-1 # 欧洲(爱尔兰)/eu-west-1 # 欧洲(伦敦)/eu-west-2
# 欧洲(米兰)/eu-south-1 # 欧洲(巴黎)/eu-west-3 # 欧洲(斯德哥尔摩)/eu-north-1
# 中东(巴林)/me-south-1 # 南美洲(圣保罗)/sa-east-1

# 定义通用变量
variable "subnet_prefix" {
description = "cidr block for the subnet"
}

# 声明云厂商/地域
provider "aws" {
region = "ap-northeast-1"
access_key = "AKIxxxxxxxxxxK7CGE"
secret_key = "7rqmTSvlxxxxxxxxxxxxx/oWbnSOc"
# 控制台 > 用户 > Security credentials 获得access和secret key
}

# 创建一个vpc
resource "aws_vpc" "first-vpc" {
cidr_block = "10.10.0.0/16" #vpc的
tags = {
Name = "first-vpc"
}
}

# 创建一个互联网网关
resource "aws_internet_gateway" "first-gw" {
vpc_id = aws_vpc.first-vpc.id

tags = {
Name = "first-gw"
}
}

# 创建默认路由表指向互联网网关
resource "aws_route_table" "first_route_table" {
vpc_id = aws_vpc.first-vpc.id

route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.first-gw.id
}

route {
ipv6_cidr_block = "::/0"
gateway_id = aws_internet_gateway.first-gw.id
}

tags = {
Name = "first_route_table"
}
}

# 在vpc中创建一个子网段(一般要创建两个,否则部分资源高可用无法创建)
resource "aws_subnet" "subnet-1" {
vpc_id = aws_vpc.first-vpc.id
cidr_block = var.subnet_prefix[0].cidr_block
availability_zone = "ap-northeast-1a"
tags = {
Name = var.subnet_prefix[0].name
}
}
resource "aws_subnet" "subnet-2" {
vpc_id = aws_vpc.first-vpc.id
cidr_block = var.subnet_prefix[1].cidr_block
availability_zone = "ap-northeast-1c"
tags = {
Name = var.subnet_prefix[1].name
}
}

# 关联子网和路由表
resource "aws_route_table_association" "a" {
subnet_id = aws_subnet.subnet-1.id
route_table_id = aws_route_table.first_route_table.id
}
resource "aws_route_table_association" "b" {
subnet_id = aws_subnet.subnet-2.id
route_table_id = aws_route_table.first_route_table.id
}

# 安全组相关
resource "aws_security_group" "allow_route" {
name = "allow_route"
description = "Allow_webserver"
vpc_id = aws_vpc.first-vpc.id

#入口流量限制
ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] #允许所有人访问
}
ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] #允许所有人访问
}
ingress {
description = "SSH from VPC"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] #允许所有人访问
}

#出口流量限制
egress {
from_port = 0
to_port = 0
protocol = "-1" #所有协议
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}

tags = {
Name = "allow_tls"
}
}

# aws创建网络接口
resource "aws_network_interface" "web-server-nic" {
subnet_id = aws_subnet.subnet-1.id #网络接口关联子网id
private_ips = ["10.10.1.10"] #指定子网中的一个ip
security_groups = [aws_security_group.allow_route.id]

# attachment { #暂时不关联设备
# instance = aws_instance.test.id
# device_index = 1
# }
}

# 分配一个EIP
resource "aws_eip" "one" {
vpc = true
network_interface = aws_network_interface.web-server-nic.id #附加到网络接口上
associate_with_private_ip = "10.10.1.10" #关联私有ip

depends_on = [aws_internet_gateway.first-gw] #EIP依赖互联网网关
}

data "aws_ami" "latest_amazon_linux" {
owners = ["amazon"]
most_recent = true
filter {
name = "name"
values = ["amzn2-ami-kernel-5.10-hvm-*-gp2"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
}

# 开启aws ec2机器
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance
resource "aws_instance" "mytest001" {
# ami = "ami-0b7546e839d7ace12"
ami = data.aws_ami.latest_amazon_linux.id
instance_type = "t2.micro"
availability_zone = "ap-northeast-1a" #确保选额可用区

key_name = "test" #密钥对配置 https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-northeast-1#KeyPairs:

network_interface { #关联网卡接口
network_interface_id = aws_network_interface.web-server-nic.id
device_index = 0 #网卡设备索引,0表示第一个
}

tags = {
Name = "testwebserver"
}

user_data = <<-EOF
#!/bin/bash
sudo yum install -y amazon-linux-extras
sudo amazon-linux-extras install nginx1
sudo systemctl enable --now nginx
EOF
}


# terraform.tfvars
#subnet_prefix = ["10.10.1.0/24","10.10.2.0/24"]
subnet_prefix = [{ cidr_block = "10.10.1.0/24",name = "subnet-1"},{ cidr_block = "10.10.2.0/24",name = "subnet-2"}]

# 执行命令
# 预演:terraform.exe plan
# 部署:terraform.exe apply
# 部署个别资源:terraform.exe apply --target aws_instance.mytest001
# 手动传递变量:terraform.exe apply -var "subnet_prefix1=10.10.1.0/24"
# 删除:terraform.exe destroy
# --auth-approve 无需确认
# 删除个别资源:terraform.exe destroy --target aws_instance.mytest001
# 查看线上资源:terraform.exe state list

# tffile terraform.tfstate apply之后线上的资源创建状态

工作空间相关文件介绍

在上面的工作空间中我定义了两个文件:main.tfterraform.tfvars

  • main.tf:tf就是Terraform,Terraform代码大部分是.tf文件,语法是HCL,当然目前也支持JSON格式的Terraform代码,暂时只以tf为例
  • terraform.tfvars:Terraform 会自动加载特殊命名的变量定义文件:文件名为 terraform.tfvars 或 terraform.tfvars.json 的文件;文件名称以 .auto.tfvars 或 .auto.tfvars.json 结尾的文件

main.tf解析

声明云厂商/ak-sk/目标地域

1
2
3
4
5
6
7
# 声明云厂商/地域
provider "aws" {
region = "ap-northeast-1"
access_key = "AKIARUxxxxxxxxxxx7CGE"
secret_key = "7rqmTSvlgSxxxxxxxxxxxxx/oWbnSOc"
# 控制台 > 用户 > Security credentials 获得access和secret key
}

这里的public_key、private_key以及region 要替换成云账户真实的ak/sk和目标地域。这里将机密信息硬编码在代码中的做法是非常错误的,这里是为了方便演示。aws/aliyun/huaweicloud等 可以通过环境变量传递以上机密信息(Linux为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#aliyun
$ export ALICLOUD_ACCESS_KEY="LTAIUrZCw3********"
$ export ALICLOUD_SECRET_KEY="zfwwWAMWIAiooj14GQ2*************"
$ export ALICLOUD_REGION="cn-beijing"

#aws
$ export AWS_ACCESS_KEY_ID="anaccesskey"
$ export AWS_SECRET_ACCESS_KEY="asecretkey"
$ export AWS_REGION="ap-northeast-1"

#huaweicloud
$ export HW_REGION_NAME="cn-north-1"
$ export HW_ACCESS_KEY="my-access-key"
$ export HW_SECRET_KEY="my-secret-key"

以上命令可以在linux终端中执行或者写入/etc/bashrc

创建一个vpc

1
2
3
4
5
6
7
# 创建一个vpc
resource "aws_vpc" "first-vpc" {
cidr_block = "10.10.0.0/16" #指定vpc私网网段
tags = {
Name = "first-vpc"
}
}

参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc

创建一个互联网网关

1
2
3
4
5
6
7
resource "aws_internet_gateway" "first-gw" {
vpc_id = aws_vpc.first-vpc.id #获取上一步创建的vpcid,指定在此vpc中创建互联网网关

tags = {
Name = "first-gw"
}
}

参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway

创建默认路由表 写入默认路由指向互联网网关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
resource "aws_route_table" "first_route_table" {
vpc_id = aws_vpc.first-vpc.id #获取上一步创建的vpcid,指定在此vpc中创建路由表

route { #ipv4的路由规则,0.0.0.0/0所有流量默认转发到上一步创建的互联网网关。即允许关联了这个路由表的子网通过互联网网关访问公网
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.first-gw.id #通过末尾.id的方式获取上一步创建的互联网网关的资源id
}

route {
ipv6_cidr_block = "::/0"
gateway_id = aws_internet_gateway.first-gw.id
}

tags = {
Name = "first_route_table"
}
}

参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table

在vpc中创建一个子网段(一般要创建两个,否则部分资源无法做到高可用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
resource "aws_subnet" "subnet-1" { #第一个子网
vpc_id = aws_vpc.first-vpc.id #获取第一步创建的vpcid
cidr_block = var.subnet_prefix[0].cidr_block #这里用到一个类似map的方式取变量值(取子网段)
availability_zone = "ap-northeast-1a" #定义在哪个可用区
tags = {
Name = var.subnet_prefix[0].name ##这里用到一个类似map的方式取变量值(取子网段的名字)
}
}
resource "aws_subnet" "subnet-2" { #第二个子网
vpc_id = aws_vpc.first-vpc.id
cidr_block = var.subnet_prefix[1].cidr_block
availability_zone = "ap-northeast-1c"
tags = {
Name = var.subnet_prefix[1].name
}
}

参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet
这里创建子网时,定义的网段,和子网的tag标签 是从变量中获取的值。在1.2节介绍工作空间时,我定义了两个文件。一个main.tf。另一个是terraform.tfvars(这个文件terraform会自动加载)
其中terraform.tfvars文件内容如下:

1
subnet_prefix = [{ cidr_block = "10.10.1.0/24",name = "subnet-1"},{ cidr_block = "10.10.2.0/24",name = "subnet-2"}]

在上面的变量定义方式类似与golang中的map,可以看成是键值对类型的数组。在1.3.5 中取值方式是

1
2
var.subnet_prefix[0].cidr_block #即取到了10.10.1.0/24
var.subnet_prefix[0].name #即取到了subnet-1

将1.3.5创建的子网和1.3.4创建的路由表进行关联

1
2
3
4
5
6
7
8
9
# 关联子网和路由表
resource "aws_route_table_association" "a" {
subnet_id = aws_subnet.subnet-1.id #获取要关联的子网id
route_table_id = aws_route_table.first_route_table.id #获取要关联的路由表id
}
resource "aws_route_table_association" "b" {
subnet_id = aws_subnet.subnet-2.id
route_table_id = aws_route_table.first_route_table.id
}

参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association

创建安全组,并放行入方向指定端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
resource "aws_security_group" "allow_route" {
name = "allow_route" #安全组名字
description = "Allow_webserver" #安全组描述
vpc_id = aws_vpc.first-vpc.id #在那个vpc中创建

#入口流量限制
ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] #允许所有人访问
}
ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] #允许所有人访问
}
ingress {
description = "SSH from VPC"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] #允许所有人访问
}

#出口流量限制
egress {
from_port = 0
to_port = 0
protocol = "-1" #所有协议
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}

tags = {
Name = "allow_tls"
}
}

参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group

在指定子网下创建一个aws网络接口,并分配一个eip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# aws创建网络接口
resource "aws_network_interface" "web-server-nic" {
subnet_id = aws_subnet.subnet-1.id #网络接口关联子网id
private_ips = ["10.10.1.10"] #指定子网中的一个ip
security_groups = [aws_security_group.allow_route.id]

# attachment { #暂时不关联设备
# instance = aws_instance.test.id
# device_index = 1
# }
}

# 分配一个EIP
resource "aws_eip" "one" {
vpc = true
network_interface = aws_network_interface.web-server-nic.id #附加到上面创建的网络接口上
associate_with_private_ip = "10.10.1.10" #关联私有ip

depends_on = [aws_internet_gateway.first-gw] #EIP依赖互联网网关,所以这里需要定义依赖项
}

参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_interface
参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip

启动一台ec2,并执行自定义的启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#查找你要用的镜像的ami
#https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-northeast-1#Images:visibility=public-images;v=3;search=:ami-0b7546e839d7ace12
data "aws_ami" "latest_amazon_linux" {
owners = ["amazon"]
most_recent = true
filter {
name = "name"
values = ["amzn2-ami-kernel-5.10-hvm-*-gp2"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
}

# 开启aws ec2机器
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance
resource "aws_instance" "mytest001" {
# ami = "ami-0b7546e839d7ace12" #写死ami
ami = data.aws_ami.latest_amazon_linux.id #获取查找出来的ami
instance_type = "t2.micro" #定义机器规格
availability_zone = "ap-northeast-1a" #确定机器所在可用区

key_name = "test" #密钥对配置 https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-northeast-1#KeyPairs:

network_interface { #关联网卡接口
network_interface_id = aws_network_interface.web-server-nic.id
device_index = 0 #网卡设备索引,0表示第一个
}

tags = {
Name = "testwebserver"
}

user_data = <<-EOF
#!/bin/bash
sudo yum install -y amazon-linux-extras
sudo amazon-linux-extras install nginx1
sudo systemctl enable --now nginx
EOF
}

output "eip" {
value = aws_eip.one.public_ip
}

参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami
参考文档:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance

  • 其中data代表利用aws插件定义的data模型对aws进行查询,例如我们在代码中利用data查询东京区域aws官方提供的Amazon Linux 2 Kernel 5.10 AMI 2.0.20220606.1 x86_64 HVM gp2 镜像的id,这样我们就不需要人工在界面上去查询相关id再硬编码到代码中,而是拿到搜索来的结果赋值给创建机器那一步。
  • 其中resource代表我们需要在云端创建的资源,在例子里我们创建的这些资源,分别是vpc,互联网网关,路由表,子网,安全组,网络接口,EIP,ec2机器,并将他们组合在一起形成一个简单的交付项目。
  • 在定义ec2时我们通过user_data定义了第一次开机时需要执行的一次性初始化脚本,脚本中定义了安装nginx并启动的动作。
  • output “eip” 将terraform执行完毕后,aws分配给网络接口的公网eip打印到控制台中。

Terraform常用操作命令

1
2
3
4
5
6
7
8
9
10
11
# 执行命令
# 预演:terraform.exe plan
# 部署:terraform.exe apply
# 部署个别资源:terraform.exe apply --target aws_instance.mytest001
# 手动传递变量:terraform.exe apply -var "subnet_prefix1=10.10.1.0/24"
# 删除:terraform.exe destroy
# --auth-approve 无需确认
# 删除个别资源:terraform.exe destroy --target aws_instance.mytest001
# 查看线上资源:terraform.exe state list

# tffile terraform.tfstate apply之后线上的资源创建状态