Terraform

Terraform

Terraform

建议大家可以阅读一下这篇中文terraform文档

英文好的读下terraform up and running

why为什么使用terraform,和如何选择iac tools

这里书中通过几项对比

Configuration management && Provisioning

配置管理还是编排工具呢

  1. Puppet && Ansible && SaltStack
  2. CloudFormation, Terraform, OpenStack Heat

如果不适用镜像工具等,同时又需要同步配置,建议同时使用

Mutable Infrastrucure && Immutable Infrastructure

可变的,不一样的 && 一样的,不变的

通常类似puppet管理的服务器,越来越多,同时非puppet模块内容的,

每个服务器都可能长得不一样,甚至每个版本的东西都可能执行的不一样,

而terraform通过packer打出来的服务器,一模一样,但是一模一样的代价

是需要部署时间,并且服务会有重启的操作。就好像重启服务,一个运行

systemctl restart xxx-service,一个把机器换个硬盘重启了

Procedural Language && Declarative Language

流程化语言还是声明式语言

ansible

  • ec2: count: 10 image: ami-xxx instance_type: t2.micro

resource "aws_instance" "example" { count = 10 ami = "ami-xxx" instance_type = "t2.micro" }

当我们尝试把数字更新到15的时候,ansible会创建另外15个服务器,而terraform则会

追加到15台服务器, terraform是有状态的,而ansible只做它应该做的事情

事务性的流程化语言按照流程处理就会结束,一次只做一件事情,类似api的功能

而声明式的则会找到上次的状态并进行再次构建,更加方便重用,类似服务的功能

有个缺点就是使用的功能局限在云厂商对于terraform的支持上,如果没有,则需要自己实现

而这通常会比较复杂

Master && Masterless

有管控机master,没有管控机master

管控机的好处:

  • 配置在一个地方,只需要管理管控机就好了
  • 有统一的配置接口,方便调用
  • 一般是后台运行,比如puppet agent可能每半个小时请求下master,然后同步配置

管控机的坏处:

  • 额外的基础设施
  • 安全性
  • 额外的维护工作,升级部署等

无管控机的好处:

  • 不用管理管控机
  • 通过一些通用组件来连接,比如ssh或者云的各种connections等

Large Community && Small Community

一般选择技术方案,也会看这个技术的社区大小。

社区决定了解决问题时间和实现的质量等各种问题。其实我们了解到的这些,社区都已经足够。

但是我们可以从几个角度看社区的大小

  • IsOpen

是否开源

  • Contributors, Stars,上次commit的时间

有多少人贡献代码,有多少颗红心,这个可以看github

  • StackOverflow有多少相关问题
  • Jobs

相关工作jd里面多少包含

Mature && Cutting Edge

成熟的,久远的 还是 持续更新的

IaC(Infrastructure as Code)代码即基础设施

为什么使用terraform?为什么使用Iac?

下面部分来自terraform up and running

self-service

自动化创建一整个服务,而不会需要了解很多奇怪的东西

speed and safety

更快更安全

Documentation

代码就是文档,通过文档可以读取对应的结构等等信息

Version control & Validation

版本控制,可以记录整个变更历史,方便回滚、测试等操作

Reuse

方便重用,主要在多云、多环境等方面

Happiness

幸福感,正常管理云资源等是比较枯燥和繁琐的,而有了IaC,我们需要做的就是不断升级我们的

版本。

terraform具体使用

精简版可以参考我这两篇文档terraform使用terraform使用和发布

自己本地试试,没有云账户,用docker也可以的, 我的例子以腾讯云为例,其他差不多

编辑器

使用前,建议配置下编辑器,

一般的编辑器都支持HCL格式的文件,这个时候去搜索下,emacs下安装terraform-mode即可

provider

provider相当于服务商,国内比如阿里云,腾讯云,华为云,国外AWS,公共的,比如docker,openstack 等等。

所有支持的providers列表, 基本上都覆盖了。

https://registry.terraform.io/browse/providers

定义一个provider很简单

1
2
3
4
5
provider "xxxclound" {
    secret_id = "xxx"
    secret_key = "xxx"
    region = "ap-shanghai"
}

packer打镜像工具

packer是使用terraform很重要的一步

建议先阅读下packer startedpacker官方文档

如果没时间可以参考下我的流程

编写hcl文件
  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
# This is packer config created by liuliancao at 20211112.
# Used for automated build with a tencent image.

variable "secret_id" {
  type    = string
  default = "xxx"
  description = "public tencent app id and key"
  # prevent showing in output when setting a plan or an apply
  sensitive = true
}
variable "secret_key" {
  type    = string
  default = "xxx"
  description = "public tencent app id and key"
  sensitive = true
}

# default image password:
# eg: 123
# the image ssh password will be 123
variable  "image_password" {
  type = string
  default = "fdsa!fea@#$"
  description = "the image ssh password."
  sensitive = true
}

# default image username
# eg: root
# the image ssh username will be root
variable  "image_user" {
  type = string
  default = "root"
  description = "the image ssh user."
}

# default provider zone
# eg: ap-shanghai-2
variable  "provider_zone" {
  type = string
  default = "ap-shanghai-2"
  description = "default provider zone, like ap-shanghai-2."
}

# default battle vpc id
# eg: vpc-123456
# will use the vpc with id vpc-123456
# attention: donnot change it if not sure
variable  "vpc_id" {
  type = string
  default = "vpc-123456"
  description = "default battle vpc id"

  validation = {
    condition = can(regex("^vpc-", var.vpc_id))
    error_message = "the variable vpc_id is not valid, it should start with vpc like vpc-123456."
  }
}

# default subnet id
# eg: subnet-123
# will use subnet with id subnet-123
variable  "subnet_id" {
  type = string
  default = "subnet-123456"

  validation = {
    condition = can(regex("^subnet-", var.subnet_id))
    error_message = "the variable subnet_id is not valid, it should start with subnet like subnet-123456."
  }
}

# default security group id
# eg: sg-123
variable  "security_group_id" {
  type = string
  default = "sg-123456"

  validation = {
    condition = can(regex("^subnet-", var.subnet_id))
    error_message = "the variable subnet_id is not valid, it should start with subnet like subnet-123456."
  }
}

# playbook file for calling packer
# eg: /data/playbook/packer-image.yml
variable "packer_playbook_yml" {
  type    = string
  default = "/data/playbook/test-battle-image.yml"
}


source "tencentcloud-cvm" "xxx" {
  #associate_public_ip_address = true
  disk_size                   = 150
  disk_type                   = "CLOUD_PREMIUM"
  image_name                  = "img-xxx"
  instance_type               = "C3.LARGE8"
  packer_debug                = true
  region                      = "ap-shanghai"
  run_tags = {
    some_tag = "${var.env}-image"
  }
  secret_id       = "${var.secret_id}"
  secret_key      = "${var.secret_key}"
  source_image_id = "img-pure"
  ssh_password    = "${var.ssh_password}"
  ssh_username    = "${var.ssh_username}"
  subnet_id       = "${var.subnet_id}"
  vpc_id          = "${var.vpc_id}"
  zone            = "${var.provider_zone}"
}

build  {
  sources = ["source.tencentcloud-cvm.xxx"]

  provisioner "ansible" {
    extra_arguments = ["-e v_1=\"${var.v1}\""]
    playbook_file   = "${var.packer_playbook_yml}"
  }
}

上面这个会创建一个临时服务器,并且对这个服务器执行ansible-playbook操作

进行额外装配,操作完成以后就会自动打镜像,失败临时packer服务器就会销毁。

实际操作命令, 具体操作可以packer相关,需要注意一个问题,调用packer

需要用=绝对路径=,因为默认云服务器可能也有一个packer,那个是db相关的工具,为了避免麻烦

请按照绝对路径操作

1
./packer build  -machine-readable -var="v1=v1_value" xxx.hcl > packer.log

resource

provider好像是告诉我们要去超市还是去批发市场,resource则是具体的买了,

针对resource,一个典型就是云服务器,云网络,这些都抽象为Resource, 也就是云厂商

卖的具体的东西,一直具体到一个EIP等等

比如aws文中的

1
2
3
4
  resource "aws_instance" "example" {
    ami = "ami-xxx"
    instance_type = "t2.micro"
  }

这个在provider列表中,对应链接里面有对应的厂商的privder使用文档

init && plan && apply

一般terraform工作流分为init–>plan–>apply–>destroy

具体请参考官方文档terraform流程

init

比如我们cd到工程目录或者我们copy一个terraform目录,这个时候需要init

init干嘛,通常会帮我们生成几个文件,如果是新项目

如果是老项目,则会安装对应的provider插件,相当于工程准备工作

init以后注意 .terraform .tfstate .tfstate.backup不要带上版本管理

你可以把需要的写进各个tf文件里面

plan

plan是计划,如果我们改了配置文件或者需要操作什么,会生成一个计划,

terraform plan > liuliancao.plan

plan会告诉我们,比如这次会怎么样,销毁几台机器还是创建几台机器,用户确认后,

可以继续后面的操作

apply

apply是应用,可能是一个plan,也可能是一个工程直接apply

一般terraform apply后,会提示确认,比如创建什么,销毁什么

destroy

当我们的资源infrasture不需要的时候,则可以进行销毁,销毁是危险操作,需要确认

工作目录等信息。

provisions

provisions具体建议看下官网provision相关 这个一般是通过connections解决,但是我一般不建议插入太多的connection

建议用同步工具比如puppet, ansible等操作

因为如果创建服务器是循环的,则可能会导致卡住, 我来说下可能用到的一些场景

remote exec

remote表示对刚刚创建的服务器使用一些命令

local exec

local就是本地操作, 这里可能会有个问题就是,我local exec ssh 这台服务器执行某个命令

或者依赖它up才会执行

这里就需要这样写一下

1
2
3
4
5
6
  resource "xxx-provider" "xxx" {
    instance_type = "xxx"
    provisioner "local-exec" {
      command = "bash ~/wait_for_ip.sh ${self.private_ip} && echo continuing..."
    }
  }
  • 第一个我们可以使用terraform self使用instance的相关信息
  • 第二个我们需要等ssh通才能执行

这也是我为啥不建议在这里写

那怎么来呢,把terraform当成资源购买的方式,如果想这个资源都有啥,尽量在镜像里面

也就是packer里面通过这种方式打好,而其他的不同的尽量收敛到注册中心等地方

如果实在需要,这段时间无法改造,我有两个办法

  • 用这种local-exec的方式,调用单个ansible执行
  • 开机任务(linux放到rc.local,windows放到计划任务)

variables

为什么说terraform是一门语言,因为它可以定义变量定义数据结构,条件等等

是hashcorp家的hcl语言方式,consul也是他们公司的,

变量可以说是非常重要的,定义好变量才是整个工作流的开始,建议阅读官方terraform变量教程

1
2
3
4
5
6
7
8
9
variable  "security_group_id" {
  type = string
  default = "sg-xxxx"

  validation = {
    condition = can(regex("^subnet-", var.subnet_id))
    error_message = "the variable subnet_id is not valid, it should start with subnet like subnet-123456."
  }
}

定义一个变量,可以定义报错信息和正则信息

如果需要保护这个变量不在apply或者plan日志里面打印,比如这个变量设置了默认值,key或者密码

则追加sensitive = true

其他地方如何引入 ${var.security_group_id}这样即可使用

尽量在资源等有必要的地方使用变量,然后variables.tf定义默认变量或者不定义

这样会提高安全性,可定制性,也更方便后面迁移到模块

关于变量需要知道

  • variables.tf是定义变量的地方,建议需要动态指定的配置,就是我们执行命令的选项类似, 请放到变量里面,相当于ansible的roles/defaults.yml roles/vars.yml
  • terraform.tfvars这种变量,表示我们执行terraform plan or apply的时候指定的变量

可以和ansible对比理解,tfvars是放到playbooks或者vars.yml里面的

data

data是数据,这个数据是资源携带的,定义data是为了方便后续使用

一个典型场景,比如,我想获取所有满足某个条件的镜像,那么我们的main.tf可能这样写

1
2
3
4
5
6
7
8
9
  data "tencentcloud_images" "my_image" {
    image_name_regex = "img-liuliancao-${var.web-version}"
  }
  # 这样在后面定义资源的时候
  resource "tencentcloud_instance" "my-server" {
    # ...
    image_id = data.tencentcloud_images.my_images.images.0.image_id
    # ...
  }

output

具体请查阅terraform outputs官网 outputs.tf的作用其实就是定义很多个data,方便各个tf使用,和模块导出给其他使用

template

模板可以定义把服务器转化成ansible的格式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
data "template_file" "ansible_inventory" {
  template = file("./templates/my-servers.tpl")
  vars = {
    servers = "${join("\n", data.tencentcloud_instances.my_server.instance_list[*].private_ip)}",
  }
}
resource "local_file" "save_inventory" {
  count = 1
  content  = data.template_file.ansible_inventory.rendered
  filename = "./ansible/inventory/servers"
}
# templates/my-servers.tpl
${servers}

state

关于state可以看terraform state官方文档

可以看下apply以后维护的.terraform.tfstate这个文件,其实里面包含了很多json信息

state可以放到云provider上面,防止丢失

默认不能同时执行terraform两次,因为会加锁,也不建议terraform的时候Ctrl-C,

这是相当危险的操作,可能会造成不一致,但是这个时候可以继续执行通常都能恢复,

最怕的是Provider Bug.

文中介绍了一种目录结构,具体可以看书116页哈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  - stage
    - vpc
    - services
      - front-app
        var.tf
        outputs.tf
        main.tf
  - global
    - s3
      outputs.tf
      main.tf
      var.tf

这里介绍了定义terraform remote state定义的方法,

首先上层模块定义output用于想依赖模块准备输出,比如下游另一个web服务需要这个db的

端口地址等信息,则可以通过调用对应的state知道

1
2
3
  output "address" {
    value = "${aws_db_instance.example.address}"
  }

获取远程state

1
2
3
4
5
6
7
8
9
  data "terraform_remote_state" "db" {
    backend = "s3"

    config {
      bucket = "your bucket name"
      key = "stage/data-stores/mysql/terraform.tfstate"
      region = "us-east-1"
    }
  }

下游准备使用

这个时候在resource里面的user_data == <<EOF #!/bin/bash echo "Hello, World!" echo "${data.terraform_remote_state.db.address}" >> index.html EOF 或者其他各个地方都可以使用这个了

这里还要注意terraform state有几个命令比较方便

  • terraform taint

taint是污染的意思,当我们对一个资源执行taint的时候,那么这个资源会被强制删除

并且创建一个新的

  • terraform state list

列出所有的资源,方便后面的操作

  • terraform state rm

当我们想把一个资源从terraform挣脱出去的时候,用state rm

这个时候,会新创建一个服务器,并且执行删除等操作的时候都不会再影响它

module

terraform里面的module类似咱们代码里面的函数,工厂等等。

模块定义

和我们直接写一样的,

workspace/ my-module/ vars.tf outputs.tf main.tf user-data.sh

引用的时候

module "my-module" { source = "workspace/my-module"

var1 = "xxx" var2 = "xxx" }

这里文中的例子是

比如A业务需要web服务器,B业务也需要web服务器,只是数量不一样,那么则非常可以使用模块

模块是函数,那么参数就是input.tf里面的var1,var2这些变量,这些是标记不同点

这里有一个技巧就是对于使用模块的一方,可以把自己相关的state文件告知模块

模块放一个变量叫state_path,模块里面就能调用调用方的不同信息,这个是动态的

具体看P139

后面文章也提到了,一个建议就是不要共用vpc,最好一个环境一个vpc或者一个workspace一个vpc

if

terraform支持条件表达式,表达式等都可以使用terraform的常见terraform函数

${var.xxx} == 0 ? 1 : 0

高阶if

配合count来实现if else count = ${var.xxx} == xxx ? 1 : 0

meta create_before_destory

说一个本地安装云provider的办法

正常情况下,都会github timeout

我们可以提前下载好,然后到workspace, 这里以腾讯云为例

1
2
3
4
5
6
  cd /tmp
  wget https://ghproxy.com/https://github.com/tencentcloudstack/terraform-provider-tencentcloud/releases/download/v1.59.18/terraform-provider-tencentcloud_1.59.18_linux_amd64.zip
  cd /data/terraform
  mkdir -p .terraform/providers/registry.terraform.io/tencentcloudstack/tencentcloud/1.59.18/linux_amd64
  unzip /tmp/terraform-provider-tencentcloud_1.59.18_linux_amd64.zip -d .terraform/providers/registry.terraform.io/tencentcloudstack/tencentcloud/1.59.18/linux_amd64/
  ./terraform init

增加terraform apply的时候并发

terraform apply -parallelism=10 这个是并发调整到10,一般可以到100这样

terraform apply的时候不要确认

terraform apply -auto-approve 注意该操作存在危险请确认后再加上

写for_each报错

A reference to "each.value" has been used in a context in which it unavailable, such as when the configuration no longer contains the value in its "for_each" expression. Remove │ this reference to each.value in your configuration to work around this error

这里意思就是说你的遍历有问题

如果用了local

注意是for_each = local.xxx 不是locals

但是定义local的时候要这样写

locals = { xxx = "" }

还要注意是for_each不是foreach,别写错了

terraform条件判断报错

The given key does not identify an element in this collection value

排查下发现是到另一个条件了,terraform console进行条件判断或者打印变量

很好用!

编写terraform测试(216)

测试类型

Unit tests单元测试

单元测试对每一段小代码执行,或者小函数

Integration tests集成测试

包含几个模块之间进行测试

Smoke tests冒烟测试

每次发布的时候执行的测试

测试流程

  • 这里文中建议的是region,state远程等变成变量
  • 通过外步脚本,创建的时候传入region等参数
  • terraform apply && terraform output检查参数
  • 整个流程用熟悉的语言编写出来就好了

编写terraform注释(219)

module注释

每一个module编写一个Readme

最好还有一个更详细的说明文档

代码注释

描述代码里面没有的东西,可能被忽略的东西,而不是写一些无用的注释

在variable.tf里面使用description描述变量的使用

示例代码

请为模块编写示例代码,这些代码可以让用户更快地了解这个模块

可以放在README,这些通常也可以放到自动化测试里面

terraform workflow工作流

plan计划

terraform plan -out=liuliancao.plan terraform apply -out=liuliancao.plan

staging进入代码

至少维护两个环境,Production和Staging

code review代码审查

production部署到生产环境

多环境适配

通过tfvars控制每个环境的不同

以liuliancao-blog为例子,version0.1需要发布到version0.2
本地调试,在liuliancao目录下

修改对应的tfvars

tfvars为source = "git::git@github.com:liuliancao/liuliancao-blog.git/blog?ref=0.1"

tfvars为source = "git::git@github.com:liuliancao/liuliancao-blog.git/blog?ref=0.2"

terraform init && terraform apply -var-file liuliancao.tfvars

测试,跑单元测试,跑集成测试,通过

terraform destroy

test分支联系QA测试

git上调整测试环境

调整测试环境的tfvars

tfvars为source = "git::git@github.com:liuliancao/liuliancao-blog.git/blog?ref=0.1"

tfvars为source = "git::git@github.com:liuliancao/liuliancao-blog.git/blog?ref=0.2"

terraform init && terraform apply -var-file test.tfvars测试指定环境指定版本

没问题terraform destory

调整到线上,git修改tfvars,并且多人review,最终完成线上变更

整体如图(来源terraform-up-and-running) [[ ../images/terraform-up-and-running01.png ]]

terraform把一台机器保留其他销毁

terraform state list terraform state rm xxx.xxx.xx[\"xxx\"] 注意反斜杠,否则会报错 terraform state rm module.xxx.tencentcloud_instance.liuliancao["ap-shanghai-5-2"] ╷ │ Error: Index value required │ │ on line 1: │ (source code not available) │ │ Index brackets must contain either a literal number or a literal string.

terraform发现引用module导致创建多个resource duplicate resource

原因可能是

  • 你引入了多次,并且可能是不一样的
  • 你module里面名字写重复了

我最近遇到vpc会出现多个的情况,就是vpc的name写成一样的了,这样会导致很奇怪的问题

xxx is a object, known only after apply

写错了资源名称了,一般建议写个depends_on = [ module.xxx ]

terraform remote module from git(terraform使用远程git目录当自己的source)

1
  source             = "git::git@liuliancao.com:liuliancao/terraform-xxx.git//x/y/z?ref=v0.1"
  • 注意格式是这样的git::git@xxxx.com也可以git::ssh//git@xxx.com
  • .git后面是//,/会找不到
  • 支持/x/y路径,但是terraform会把整个git目录都拉到.terraform/modules下,可能是为了了解git信息

如果你觉得不好,那就每个模块都写一个git目录,但是这样说实话管理起来比较麻烦

还有一种解决办法是写一个module git专门同步,然后其他的用相对目录引入

remote backend

官方文档有,这里不说了

cos

也是支持的

etcdv3

注意这个报错 {"level":"warn","ts":"2021-12-14T18:10:06.791+0800","caller":"clientv3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"endpoint://client-d1cc2ec1-b919-405b-b6b1-da553a1b7e64/xxx:2379","attempt":0,"error":"rpc error: code = NotFound desc = etcdserver: requested lease not found"} Error loading state: Failed to lock state in etcd: etcdserver: requested lease not found.

原因是刚开始没有东西可以lock

解决办法:修改backend里面的lock=true为lock=false创建完成以后,再改回来

provisoner command

1
2
self.public_ip is null
The expression result is null. Cannot include a null value in a string template.

通过条件表达式解决

1
command = self.public_ip == null ? "echo nothing" : "do something with self.public_ip"

用模板的时候报错

20: template = file("./version-hosts.tpl") │ │ Invalid value for "path" parameter: no file exists at ./version-hosts.tpl; this function works only with files that are distributed as part of the │ configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of │ that resource. ╵ 最好用../而不是./

建议汇总

  • 使用一个称手的编辑器写terraform,并且加上版本控制
  • 使用terraform fmt格式化代码或者编辑器内嵌
  • 慎用-auto-approve
  • 不要用一个vpc,尝试一个环境对应一个,并且有对应的测试vpc等信息, 总之维护一套 测试环境
  • count有限制,最大是1024,如果是做一些均衡策略模块,通常需要限制这个到1024
  • 给你写的模块通过git tag加上一个版本
  • 通过tfvars维护一些default变量而不是永远写variables default, 这样的好处是 类似不同环境配置文件一样,最大限度不破坏代码,而我们需要配置下钩子或者pipeline或者 部署脚本嵌入
  • 一旦使用了terraform,尽量所有资源通过terraform管理,如果有其他的,使用terraform import导入
  • 永远使用plan来探测可能的错误,特别注意destory的部分
  • 使用create_before_destory来控制资源的创建, 使用prevent_destory控制需要保护的资源
  • 资源名称等的名字变化不是小问题,可能会带来资源重建,需要谨慎,尽量使用create_before_destory
  • 对已有资源如果希望进行管理可以通过terraform import方式进行导入(具体 怎么import一般在proivder对应的resource最后一行都有), import完成以后 可以进行管理或者terraform state show 具体resource 生成tf文件编写