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 具备以下几个主要特点:
- 基础设施即代码(IaC, Infrastructure as Code)
Terraform 基于一种特定的配置语言(HCL, Hashicorp Configuration Language)来描述基础设施资源。由此,可以像对待任何其他代码一样,实现对所描述的解决方案或者基础架构的版本控制和管理。同时,通用的解决方案和基础架构可以以模板的形式进行便捷的共享和重用。
- 执行计划(Execution Plans)
Terraform 在执行模板前,运行 terraform plan 命令会先通过解析模板生成一个可执行的计划,这个计划展示了当前模板所要创建或变更的资源及其属性。操作人员可以预览这个计划,在确认无误后执行 terraform apply 命令,即可完成对所定义资源的快速创建和变更,以免发生一些超预期的问题。
- 资源拓扑图(Resource Graph)
Terraform 会根据模板中的定义,构建所有资源的图形,并且以并行的方式创建和修改那些没有任何依赖资源的资源,以保证执行的高效性。对于有依赖资源的资源,被依赖的资源优先执行。
- 自动化变更(Change Automation)
不论多复杂的资源,当模板中定义的资源内容发生变更时,Terraform 都会基于新的资源拓扑图将变更的内容plan 出来,在确认无误后,只需一个命令即可完成数个变更操作,避免了人为操作带来的错误。
https://www.terraform.io/downloads
Linux安装
1 2 3 4 5 6 7 8
| yum install -y yum-utils
yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.rep
yum -y install 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 install terraform
|
资源创建
这里以AWS云为例,实现一套简单的资源创建:
- 创建一个vpc
- 创建一个互联网网关
- 创建一个路由表并写入默认路由指向第2步创建的互联网网关
- 在vpc中创建两个子网(一般要创建两个,否则部分资源无法高可用如RDS)
- 将子网和路由表进行关联(否则子网下创建的资源无法访问外网)
- 创建一个安全组,并写入入放行规则 放行指定端口(给ec2机器备用)
- 在其中一个子网下创建一个网络接口(给ec2机器备用),关联第6步创建的安全组并分配一个EIP
- 创建一台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.tf
、terraform.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打印到控制台中。