说明:这篇文章,是围绕Aliyun Provider展开,其他厂商的可自行参考官网。
为了能够在本机更快速、高效执行TF脚本,本文先做一个配置,来实现加速相关Provider下载。
一、加速Aliyun Provider读取
每次运行 terraform init 命令时,Terraform会下载部署所需要的Provider,每次下载耗时很长,我们可以提前下载好Provider,例如下载到 /srv/terraform-plugins 目录下(Provider下载路径)
注意:目录格式如下:
/srv/terraform-plugins/registry.terraform.io/aliyun/alicloud/1.127.0/darwin_amd64/terraform-provider-alicloud_v1.127.0
需要按照GO语言插件的规范
1.1 使用Provider缓存
- 添加环境变量:
export TF_PLUGIN_CACHE_DIR="/srv/terraform-plugins"
- 在~/.terraformrc中增加plugin_cache_dir:
plugin_cache_dir = "/srv/terraform-plugins"
优劣:如果所需provider在缓存中不存在,会去官网下载缺失的provider。但在搜索缓存前会先去官网查询一个version文件,如果网络不通会直接报错(偶尔terraform会被墙)。
注意:配置完之后,可以通过命令行查看是否生效:
# tf -version
Terraform v1.0.1
on darwin_amd64
+ provider registry.terraform.io/aliyun/alicloud v1.127.0
1.2 指定Terraform 搜寻Provider的路径
在Terraform init时,使用一下命令指定Provider的路径,Terraform会在指定路径下搜寻可用的Provider。
terraform init -plugin-dir=/srv/terraform-plugins
优劣:不受网络影响,但如果所需provider在路径下不存在,会直接报错,不会去官网搜索。
二、配置认证
方法一:静态认证
# Configure the Alicloud Provider
terraform {
required_providers {
alicloud = {
source = "aliyun/alicloud"
version = "1.127.0"
}
}
required_version = ">=0.12"
}
provider "alicloud" {
access_key = "###"
secret_key = "####"
region = "cn-shanghai"
}
resource "alicloud_vpc" "vpc_production" {
vpc_name = "poc1"
cidr_block = "10.0.0.0/24"
}
注意,这里我们指定了providers的版本。如果不指定的话执行会如何呢?
不指定版本
Initializing provider plugins...
- Reusing previous version of aliyun/alicloud from the dependency lock file
- Finding latest version of hashicorp/alicloud... 它会去官网搜索最新版本
- Using previously-installed aliyun/alicloud v1.127.0
- Installing hashicorp/alicloud v1.127.0...
如果没有指定版本的话,就默认会去寻找hashicorp/这个namespace下面的包。
指定版本
- Reusing previous version of aliyun/alicloud from the dependency lock file
- Using previously-installed aliyun/alicloud v1.127.0
以上脚本执行完之后可以查看到已创建的资源
$ tf show
resource "alicloud_vpc" "vpc_production" {
cidr_block = "10.0.0.0/24"
id = "vpc-uf62e6h4qu0zohqaf6k8g"
name = "poc1"
route_table_id = "vtb-uf6blxigmdncups9w23n6"
router_id = "vrt-uf60j1fkrqhgq8dvyhbld"
router_table_id = "vtb-uf6blxigmdncups9w23n6"
status = "Available"
vpc_name = "poc1"
}
方法二:环境变量
provider "alicloud" {}
设置环境变量
$ export ALICLOUD_ACCESS_KEY="anaccesskey"
$ export ALICLOUD_SECRET_KEY="asecretkey"
$ export ALICLOUD_REGION="cn-beijing"
$ terraform plan
方法三、ECSRole
如果是通过ECS来运行TF脚本的话,那可以在控制台上配置角色。
provider "alicloud" {
ecs_role_name = "terraform-provider-alicloud"
region = "${var.region}"
}
方法四、Assume Role
provider "alicloud" {
access_key = local.access_key
secret_key = local.secret_key
region = local.region
alias = "member_account_${account_id}"
assume_role {
role_arn = "acs:ram::${account_id}:role/RD"
session_name = "XXX"
session_expiration = 999
}
}
优势:在多账号管理场景下,通过主账号的AK/SK可以Assume 到成员账号里面的特定角色去执行相关的配置。
三、常用命令
3.1 资源管理
Terraform对资源的管理主要是对资源生命周期的管理,即通过命令实现对Terraform模板中所定义资源的创建,修改,查看和删除。
- terraform plan:资源的预览plan命令用于对模板中所定义资源的预览,主要用于以下几个场景:
- 预览当前模板中定义的资源是否符合管理预期,和Markdown的预览功能类似。
- 如果当前模板已经存在对应的state文件,那么plan命令将会展示模板定义与state文件内容的diff结果,如果有变更,会将结果在下方显示出来。
- 对DataSource而言,执行plan命令,即可直接获取并输出所要查询的资源及其属性。
- terraform apply:资源的新建和变更apply命令用于实际资源的新建和变更操作,为了安全起见,在命令运行过程中增加了人工交互的过程,即需要手动确认是否继续,当然也可以通过--auto-approve参数来跳过人工确认的过程。apply命令适用于以下几种场景:
- 创建新的资源。
- 通过修改模板参数来修改资源的属性。
- 如果从当前模板中删除某个资源的定义,apply命令会将该资源彻底删除。可以理解为“资源的移除也是一种变更”。
- terraform show:资源的展示show命令用于展示当前state中所有被管理的资源及其所有属性值。
- terraform destroy:资源的释放destroy命令用于对资源的释放操作,为了安全起见,在命令执行过程中,也增加了人工交互的过程,如果想要跳过手动确认操作,可以通过--force参数来跳过。terraform destroy默认会释放当前模板中定义的所有资源,如果只想释放其中某个特定的资源,可以通过参数-target=<资源类型>.<资源名称>来指定。
- terraform import:资源的导入import命令用于将存量的云资源导入到terraform state中,进而加入到Terraform的管理体系中,适用的场景包含但不限于以下几种:
- 从来没有使用Terraform管控过任何资源,当前所有的存量云资源都是通过控制台,阿里云CLI,ROS或者直接调用API创建和管理的,现在想要切换为Terraform管理。
- 在不影响资源正常使用的前提下,重构资源模板中的资源定义。
- 阿里云的Provider进行了兼容性升级,新版Provider对原有模板中所定义的资源支持了更多的参数,需要把最新的参数同步进来。
示例:在控制台手动添加一个VPC,然后通过TF将这个VPC导入到当前的state文件里面。
一、先定义TF脚本
resource "alicloud_vpc" "vpcdemo" {
id = "vpc-uf6p6ahsqnki"
vpc_name = "vpc22"
cidr_block = "172.16.0.0/12"
}
二、执行导入
$ tf import alicloud_vpc.vpcdemo vpc-uf6p6ahsqnkirlellz2hi
alicloud_vpc.vpcdemo: Importing from ID "vpc-uf6p6ahsqnki"...
alicloud_vpc.vpcdemo: Import prepared!
Prepared alicloud_vpc for import
alicloud_vpc.vpcdemo: Refreshing state... [id=vpc-uf6p6ahsqnki]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
- terraform taint:标记资源为被污染taint命令用于把某个资源标记为被污染状态,当再次执行apply命令时,这个被污染的资源将会被先释放,然后再创建一个新的,相当于对这个特定资源做了先删除后新建的操作。命令的详细格式为: terraform taint <资源类型>.<资源名称> ,如:
$ terraform taint alicloud_vswitch.this
Resource instance alicloud_vswitch.this has been marked as tainted.
- terraform untaint:取消被污染标记untaint命令是taint的逆向操作,用于取消被污染标记,使其恢复到正常的状态。命令的详细格式和taint类似为:terraform untaint <资源类型>.<资源名称>
- terraform output:打印出参及其值如果在模板中显示定义了output参数,那么这个output的值将在apply命令之后展示,但plan命令并不会展示,如果想随时随地快速查看output的值,可以直接运行命令 terraform output :
$ terraform output
vswitchId = vsw-gw8gl31wz********
可以将有用的信息保存到output里面。这样就比较方便查看输出内容。
3.2 状态管理
- terraform state list:列出当前state中的所有资源。state list命令会按照 <资源类型>.<资源名称> 的格式列出当前state中存在的所有资源(包括datasource)
- terraform state show:展示某一个资源的属性state show命令按照Key-Value的格式展示出特定资源的所有属性及其值,命令的完整格式为 terraformstate show <资源类型>.<资源名称>
- terraform state pull:获取当前state内容并展示state pull命令用于原样展示当前state文件数据,类似Shell下的cat命令
- terraform state rm:移除特定的资源state rm命令用于将state中的某个资源移除,但是实际上并不会真正删除这个资源,命令格式为:terraformstate rm <资源类型>.<资源名称>
- terraform state mv:变更特定资源的存放地址如果想调整某个资源所在的state文件,可以通过state mv命令来完成,类似于Shell下的mv命令,这个命令的使用有多种选项,可以通过命令 terraform state mv --help 来详细了解。本文只介绍最常用的一种:terraform state mv --state=./terraform.tfstate --state-out=
/terraform-target.tfstate <资源类型>.<资源名称a> <资源类型>.<资源名称b> - terraform refresh:刷新当前staterefresh命令可以用来刷新当前State的内容,即再次调用API并拉取最新的数据写入到state文件中。
3.3 其他命令
- terraform init:初始化加载模块init用来初始化加载所需的模块,包括Provider,Provisioner,Module等。如果有新的Module需要init一下。
- terraform graph:输出当前模板定义的资源关系图每个模板定义的资源之间都存在不同程度的关系,如果想看资源关系图,可以使用命令terraform graph
- terraform validate:验证模板语法是否正确Terraform模板的编写需要遵循其自身定义的一套简单的语法规范,编写完成后,如果想要检查模板是否存在语法错误或者在运行plan和apply命令的时候报语法错误,可以通过执行命令terraform validate来检查和定位错误出现的详细位置和原因。
四、基本语法
4.1 Provider: 基础设施管理驱动
【Provider】 是一个与Open API直接交互的后端驱动,Terraform 就是通过Provider来完成对基础设施资源的管理的。不同的基础设施提供商都需要提供一个Provider来实现对自家基础设施的统一管理。目前Terraform目前支持超过几百种的providers,大多数云平台的Provider插件均已经实现了,阿里云对应的Provider为 alicloud。
在操作环境中,Terraform和Provider是两个独立存在的package,当运行Terraform时,Terraform会根据用户模板中指定的provider或者resource/datasource的标志自动的下载模板所用到的所有provider,并将其放在执行目录下的一个隐藏目录 .terraform 下。通常Provider都包含两个主要元素 resource 和 data source。
4.1.1 Resource: 基础设施资源和服务的管理
在Terraform中,一个具体的资源或者服务称之为一个resource,比如一台ECS 实例。每个特定的resource包含了若干可用于描述对应资源或者服务的属性字段,通过这些字段来定义一个完整的资源或者服务,比如实例的名称(name),实例的规格(instance_type)等。
定义一个Resource的语法非常简单,通过 resource 关键字声明:
resource "alicloud_instance" "e1" {}
4.1.2 Data Source:基础设施资源和服务的查询
对资源的查询是运维人员或者系统最常使用的操作,比如,查看某个region下有哪些可用区,某个可用区下有哪些实例规格,每个region下有哪些镜像,当前账号下有多少机器等,通过对资源及其资源属性的查询可以帮助和引导开发者进行下一步的操作。
在Terraform 中,Data Source 提供的就是一个查询资源的功能,每个data source实现对一个资源的动态查询,Data Souce的结果可以认为是动态变量,只有在运行时才能知道变量的值。
data "alicloud_instance_types" "default" {
availability_zone = data.alicloud_zones.default.zones.0.id
cpu_core_count = 2
memory_size = 4
}
resource "alicloud_instance" "web" {
image_id = data.alicloud_images.default.images[0].id
instance_type = data.alicloud_instance_types.default.instance_types[0].id
instance_name = "my-first-vm"
system_disk_category = "cloud_ssd"
...
}
4.1.3 State:保存资源关系及其属性文件的数据库
Terraform创建和管理的所有资源都会保存到自己的数据库上,这个数据库不是通常意义上的数据库(MySQL,Redis等),而是一个文件名为 terraform.tfstate 的文件,在Terraform 中称之为 state ,默认存放在执行Terraform命令的本地目录下。这个 state 文件非常重要,如果该文件损坏,Terraform 将认为已创建的资源被破坏或者需要重建(实际的云资源通常不会受到影响),因为在执行Terraform命令是,Terraform将会利用该文件与当前目录下的模板做Diff比较,如果出现不一致,Terraform将按照模板中的定义重新创建或者修改已有资源,直到没有Diff,因此可以认为Terraform是一个有状态服务。
当涉及多人协作时不仅需要拷贝模板,还需要拷贝 state 文件,这无形中增加了维护成本。幸运的是,目前Terraform支持把 state 文件放到远端的存储服务 OSS 上或者 consul 上,来实现 state 文件和模板代码的分离。具体细节可参考官方文档Remote State
注意:比如一开始通过TF管理资源,不小心把State文件给删除掉了。这个时候需要将存量资源再导入回来。
4.1.4 Backend:存放State文件的载体
正如上节提到,Terraform 在创建完资源后,会将资源的属性存放在一个 state 文件中,这个文件可以存放在本地也可以存放在远端。存放 state 文件的载体就是 Backend 。
Backend 分为本地(local)和远端(remote)两类,默认为本地。远端的类型也非常多,目前官方网站提供的有13种。
使用远端的Backend,既可以降低多人协作时对state的维护成本,而且可以将一些敏感的数据存放在远端,保证了数据的安全性。
示例:
terraform {
backend "oss" {
bucket = "bucket-with-terraform-state"
prefix = "path/mystate"
key = "1.tfstate"
region = "cn-hangzhou"
tablestore_endpoint = "https://m02109xs.cn-hangzhou.ots.aliyuncs.com"
tablestore_table = "slock"
# 需要有权限访问OSS与OTS,其中OTS是用于加锁,oss是保存state数据
ecs_role_name = "EcsRamRole"
}
}
4.2 Provisioner: 在机器上执行操作的组件
Provisioner 通常用来在本地机器或者登陆远程主机执行相关的操作,如 local-exec provisioner 用来执行本地的命令, chef provisioner 用来在远程机器安装,配置和执行chef client, remote-exec provisioner 用来登录远程主机并在其上执行命令。
Provisioner 通常跟 Provider一起配合使用,provider用来创建和管理资源,provisioner在创建好的机器上执行各种操作。
思考:结合Provider + Provisioner就可以实现基础设施层面的IaC加上应用配置层面的。完整的一套配置管理工具
4.3 Module: 简化你的TF模板
Module 是 Terraform 为了管理单元化资源而设计的,是子节点,子资源,子架构模板的整合和抽象。在实际复杂的技术架构中,涉及到的资源多种多样,资源与资源之间的关系错综复杂,资源模版的编写,扩展,维护等多个问题的成本都会不断增加。将多种可以复用的资源定义为一个module,通过对 module 的管理简化模板的架构,降低模板管理的复杂度,这就是module的作用。
对资源进行分类,将每一类资源用一个单独的目录进行管理,最后用一个模板来管理所有的目录,进而完成对所有资源及资源关系的串联,如下所示:
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│ ├── vpc/
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ ├── slb/
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ ├── ecs/
│ ├── rds/
│ ├── oss/
接下来,用统一的模版main.tf将这些目录关联起来,如下所示:
// VPC module
module "vpc" {
source = "./modules/vpc"
name = "new-netwtok"
...
}
// Web Tier module
module "web" {
source = "./modules/ecs"
instance_count = 2
vswitch_ids = "${module.vpc.this_vswitch_ids}"
...
}
4.4 语法
4.4.1 数据类型
JSON格式 | TF语法 |
Boolean | bool true / false |
Number | number |
String | string <<EOT hello world EOT 如果要保持格式可以这样写: value=<<-EOT hello world EOT 生成JSON或YAML。可以通过关键字jsonencode如: example = jsonencode( { a=1 b="hello" }) |
Object | map(...) {name="",age=52} local.map["keyname"] 获取map中的值 |
Array | list(...) ["",""] local.list[3] 获取第三个元素值 |
Null | null 不带引号的符号表示null |
逻辑指令
- %{if
} / {else} / {endif}
"Hello, %{if var.name != "" }${var.name}%{else} unnamed %{endif}"
- %{for
in } / %{endfor}
<<EOT
%{ for ip in aws_instance.example.*.private_ip }
server ${ip}
%{ endfor }
EOT
<<EOT
%{ for ip in aws_instance.example.*.private_ip ~}
server ${ip}
%{ endfor ~}
EOT
操作符
- * / % + - > >= < <= == != && ||
- 内置函数。min() keys()
- 三元表达式: condition ? true : false
- for表达式. [for s in var.list: upper(s)], [for k,v in var.map: length(k) + length(v)]
locals {
admin_users = {
for name, user in var.users : name => user
if user.is_admin
}
regular_users = {
for name, user in var.users : name => user
if !user.is_admin
}
}
- Grouping results
variable "users" {
type = map(object({
role = string
}))
}
locals {
users_by_role = {
for name, user in var.users : user.role => name...
}
}
内转函数
- Encoding
- String
- Numeric
- Collection
- Filesystem
- Hash and Crypto
- IP Network
- Date Time
4.4.2 代码注释
支持几种类型:
- // # /***/
4.4.3 部分关键字
- variable
variable "access_key" {
type = string #或list(string)
default = ""
description = ""
}
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 "user_information" {
type = object({
name = string
address = string
})
sensitive = true
}
resource "some_resource" "a" {
name = var.user_information.name
address = var.user_information.address
}
变量类型支持:
- list
- set
- map
- object
- tuple
- output
output "application_account_id" {
value = ""
description = "Output application account id."
sensitive = true
depends_on = ["",""]
}
- local
locals {
# Ids for multiple sets of EC2 instances, merged together
instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}
locals {
# Common tags to be assigned to all resources
common_tags = {
Service = local.service_name
Owner = local.owner
}
}
# 使用local的值
resource "aws_instance" "example" {
# ...
tags = local.common_tags
}
相关参数说明
- resource
resource "aws_instance" "server" {
count = 4 # create four similar EC2 instances
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
Name = "Server ${count.index}"
}
}
结合for_each与count
variable "subnet_ids" {
type = list(string)
}
resource "aws_instance" "server" {
# Create one instance for each subnet
count = length(var.subnet_ids)
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = var.subnet_ids[count.index]
tags = {
Name = "Server ${count.index}"
}
}
- terraform
terraform {
backend "oss" {
bucket = "bucket-with-terraform-state"
prefix = "path/mystate"
key = "1.tfstate"
region = "cn-hangzhou"
tablestore_endpoint = "https://11.cn-hangzhou.ots.aliyuncs.com"
tablestore_table = "statelock"
# 是为了做OSS的权限用的
ecs_role_name = "EcsRamRole"
}
required_providers {
alicloud = {
source = "aliyun/alicloud"
version = "1.127.0"
}
}
required_version = ">=0.12"
}
4.5 代码片段赏析
TBD
五、解决方案
5.1 纳管存量资源
这个解决方案用于以下4个场景:
- 场景一:长期使用控制台、阿里云CLI、资源编排服务或者直接调用API创建和管理资源,初次使用Terraform的场景。
- 场景二:长期使用Terraform管理资源,如果通过控制台对单个云资源做属性变更,希望保持原有的资源状态(State)一致的场景。
- 场景三:所有资源都定义在一个模板中,想要对原有模板进行重构拆分,以降低随着资源不断增多而带来的模板和state的管理复杂度的场景。
- 场景四:想要将新版Provider中新增的参数同步到原文档中的场景。
Terraform对资源的导入可以分为三个步骤:
- 获取资源ID:基于资源ID查询资源并获取其属性。
- 模板声明所要导入的资源:模板驱动,即使是要导入的资源,也需要在模板中进行声明。
- 补齐资源模板定义:导入成功后,需要根据资源属性补齐已经在模板中声明的资源定义。
1、获取资源ID。对资源ID的获取可以通过Web控制台,CLI,API等多种方式,最简单的方式是通过Terraform的DataSource,输入简单的查询条件,例如获取一个ECS实例:
data "alicloud_instances" "instances_ds" {
name_regex = "i*"
}
output "instance_ids" {
value = "${data.alicloud_instances.instances_ds.ids}"
}
2、模板声明所要导入的资源。和创建资源一样,在导入资源前,也需要在模板中进行资源声明,以便指定所要导入的资源在State中的存放路径。如下所示,声明一个ECS实例:
resource "alicloud_instance" "e1" {}
简单的声明之后,无需定义具体的参数即可开始资源的导入操作。在Terraform中,导入一个资源的操作通过import命令来完成,完整的命令格式为terraform import <资源类型>.<资源标识> <资源id>.格式如下:
$ tf import alicloud_instance.e1 i-bp18s06o9bc53n32
alicloud_instance.e1: Importing from ID "i-bp18s06o9bc53n32"...
alicloud_instance.e1: Import prepared!
Prepared alicloud_instance for import
alicloud_instance.e1: Refreshing state... [id=i-bp18s06o9bc53n32]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
3、补齐资源模板定义。由于模板中没有完成对所导入资源的详细定义,因此,资源导入成功后,模板内容与State存储的内容存在差异(State里面的内容是完整的,但模板的内容是空白的).
为了保持资源模板与资源状态的一致,需要在模板中手动补齐缺失的参数定义,直到运行plan不会再有变更信息为止.所要补齐的内容主要以那些引起更新的字段为主,补齐完成后运行terraform plan进行测试:
No changes. Infrastructure is up-to-date.
在执行plan之后直到出现No changes 表示完成
5.2 通过TF+Ansible实现整体自动化运维
思路:TF中的provider用来创建和管理资源,provisioner在创建好的机器上执行各种操作。
5.3 通过Jenkins + TF搭建基础设施的CI/CD
TBD
六、生态体系
6.1 合规审计:terraform-compliance
- terraform-compliance用于合规审计,优点在于该插件是和provider无关的。
- 用户可以自定义合规规则(使用Behavior-Driven Development
——目前常用的BDD测试框架有Ruby中的Cucumber,Python中的Behave等),比如“不能给任何Ram角色赋予AdministratorAccess权限”,这些规则存储在 features 中;
- Cucumber中几个专业术语
- Features文件:
- Feature:功能,下面可以由多个Scenario
- Scenario:场景,下面可以由多个Steps
- Steps:步骤使用Given、When、Then、But、And这些关键词
- Step_definitions文件:根据Features文件中的Steps编写测试代码
- 在Terraform plan后,将plan结果存储在 .out文件 中,使用terraform-compliance对两者进行比对,判断是否合规。
7、参考资料
Alibaba Cloud Provider
官方手册
GOOGLE相关文章