terraform和发布

本篇继续介绍terraform和发布相关的一些实践。

terraform

terraform是什么

terraform是一种云上的编排工具,code as infrastructure,代码即基础设施。

terraform可以做什么

从官网的user case中可以看出,terraform可以做

  • Multi-Tier Applications(多层应用发布)

包含多个组件(比如redis,webserver,slb等应用)

  • Self-Service Clusters(自服务集群)

集群化部署一个较大规模的集群(这个集群提供一些功能,并被客户所使用)

  • Software Demos(软件demo)

可以方便的类似Dockerfile快速部署一个软件demo展示

  • Disposable Environments(临时环境)

使用terraform可以快速搭建一个临时测试环境,方便qa或者dev进行测试

  • Software Defined Networking(软件定义网络)

通过terraform的一些逻辑,可以灵活创建和管理网路

  • Resource Schedulers(资源调度) && Multi-Cloud Deployment(多云部署)

通过不同provider(比如tengxunyun, aliyun等),实现多云、多区域、多分区、多vpc等维度调度

terraform工作流

典型的terraform操作方式是 编写terraform代码–>terraform init–>terraform plan–>terraform apply–>terraform destroy

terraform组件和一些概念

  • language

terraform的功能实现依赖于terraform的代码描述语言 包含常见的变量,循环,数组,map,条件表达式等等

  • provider

常见云厂商支持providers列表 https://registry.terraform.io/browse/providers

  • resource && data

在terraform的代码主结构里面 通过resource定义购买的资源(这个资源可能是文件,模版,服务器,子网,vpc等) 通过data存储provider提供的数据(这个数据可能是服务器列表,标签列表等序列化数据)

  • module

是一种代码编排方式,terraform可以编写模块,通过模块,可以避免代码冗余,类似于类(class)

  • cli
 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
    # 用于初始化包含插件文件目录等等
    init          Prepare your working directory for other commands
    # 验证配置文件是否合法
    validate      Check whether the configuration is valid
    # 显示即将的改变,比如摧毁和创建啥
    plan          Show changes required by the current configuration
    # 通过传-var="x=y"类似参数或者修改配置文件,执行修改
    apply         Create or update infrastructure
    # 这个会销毁之前创建的所有东西
    destroy       Destroy previously-created infrastructure

    console       Try Terraform expressions at an interactive command prompt #
    fmt           Reformat your configuration in the standard style
    force-unlock  Release a stuck lock on the current workspace
    get           Install or upgrade remote Terraform modules
    graph         Generate a Graphviz graph of the steps in an operation
    import        Associate existing infrastructure with a Terraform resource
    login         Obtain and save credentials for a remote host
    logout        Remove locally-stored credentials for a remote host
    output        Show output values from your root module
    providers     Show the providers required for this configuration
    refresh       Update the state to match remote systems
    show          Show the current state or a saved plan
    state         Advanced state management
    taint         Mark a resource instance as not fully functional
    test          Experimental support for module integration testing
    untaint       Remove the 'tainted' state from a resource instance
    version       Show the current Terraform version
    workspace     Workspace management
  • target

当我们需要apply只针对于某类资源(比如文件更新,某个服务器状态修改)的时候,可以用-target=指向某个特定资源 或者特定模块

  • state

通过taint可以让一个资源变脏,再次apply的时候,该资源在生命期内会被销毁重建,untaint可以让它恢复而不被销毁 创建 state list列出所有资源(对应工程目录下的tfstate文件) state rm可以让一个资源不再被跟踪,当再次apply的时候,会增补一个资源,这个资源将成为孤儿

  • provisioner

通过provisioner我们可以在创建资源的时候额外执行想执行的操作,比如file,ansible,ssh,remote_exec, local_exec等,这是非常重要的一个功能

packer

packer是什么

packer官网 packer其实和docker名字有点像,本质是一个镜像工具,也隶属于terraform公司

packer常用功能

根据基础镜像通过pcl文件(加一系列操作)打一个新的云镜像(artifacts)

制作镜像

 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
source "tencentcloud-cvm" "xxx" {
  disk_size                   = 150
  disk_type                   = "CLOUD_PREMIUM"
  image_name                  = "img-${var.xxx}"
  instance_type               = "xxx"
  packer_debug                = true
  region                      = "ap-xxx"
  run_tags = {
    xxx = "xxx"
  }
  host_name = "xxx"
  secret_id       = "${var.secret_id}"
  secret_key      = "${var.secret_key}"
  # op-battle-pure
  #source_image_id = "img-72nemnfl"
  source_image_id = "img-xxx"
  security_group_id = "sg-xxx"
  #ssh_host = "private"
  ssh_password    = "xxxx"
  ssh_username    = "root"
  subnet_id       = "subnet-xxxx"
  vpc_id          = "vpc-xxxx"
  zone            = "ap-xxx"
}

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

  provisioner "ansible" {
    extra_arguments = ["-e variablea=\"${var.variablea}\"", "-e variableb=\"${var.variableb}\""]
    playbook_file   = "xxx.yml"
  }
}

这里其实是创建一个packer服务器,并对其进行ansible操作,类似也可以执行其他provisioner操作

购买服务器

v1版本

直接购买服务器,通过传入chicken和team的变量,对应实际购买的数量

1
2
3
4
  resource "tencentcloud_instance" "a" {
    count = var.a
    ...
  }
items advantages disadvantages
specify the count easy to scale out/in dangerous

发现问题:每次执行terraform的时候都需要指定a的count,一旦填错了就会导致销毁,及其危险,所以count默认不修改

v2版本

由于服务需要支持热发布,为了安全, 对购买的服务器分为动态区和静态区,分区操作,以最大可能屏蔽未知风险, 并且开机即开服

items advantages disadvantages
two module with purchase as start only affects different zone the host's load is too high because of ssh

发现问题: 在购买服务器的时候执行ansible或者ssh会带来极大的性能开销,单台服务器无法满足

v3版本

安排两台服务器,分别操作strict区和active区, 并且把开服放到后续让ansible进行批量并发操作,开服时间从30min以上缩减到 5min(ssh顺利,不顺利需要10min以上)

 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
resource "tencentcloud_instance" "a" {
  for_each = toset(local.a_names)
  instance_name              = "a-s-${each.value}-${index(local.a_names, each.value)}"
  availability_zone = var.your_AZs[index(local.a_names, each.value) % length(var.your_AZs)]
  image_id                   = data.tencentcloud_images.xxx.images.0.image_id
  hostname = "typea"
  instance_type              = var.a_attributes.instance_type
  system_disk_type           = var.a_attributes.system_disk_type
  system_disk_size           = var.a_attributes.system_disk_size
  allocate_public_ip         = true
  internet_max_bandwidth_out = 100
  security_groups            = ["xxx"]
  vpc_id = "vpc-xxx"
  subnet_id = tencentcloud_subnet.a_subnets[index(local.a_names, each.value)%local.a_subnet_count].id
  running_flag = var.a_running_flag
  placement_group_id         = var.a_placement_groups[index(local.a_names, each.value) % length(var.a_placement_groups)]
  provisioner "local-exec" {
    command = "[ \"${var.plans}\" == \"rolling\" ] && { bash scripts/wait_until_ip.sh ${self.private_ip} &&  ansible-playbook -i \"${self.private_ip},\" /data/playbook/test-a.yml -e plans=${var.plans}   -e type=a >> /data/xxx/ansible/sa-${var.plans}.log; } || echo nothing will do"
  }

  tags = {
    op_type: "a"
  }
  lifecycle {
    #prevent_destroy = true
    ignore_changes = [
      hostname,
      tags
    ]
  }
}

分享2个脚本

 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
#!/usr/bin/env bash
# used for wait until all hosts is ready by lqx at 2021/07/22.
IP=$1
CHECKFILE="/data/terraform/scripts/check_ssh.sh"
echo $1 > /tmp/$1
for i in {1..30};do
   bash $CHECKFILE -f /tmp/$1 -uroot -p 22 | grep -q bad
   if [[ $? -eq 0 ]];then
      sleep 10
      continue
   else
      rm /tmp/$1
      break
   fi
done
#!/usr/bin/env bash
# used for wait until all hosts is ready by lqx at 2021/07/22.

[ -z $1 ] && HOSTSFILE="/data/terraform/ansible/inventory/hosts" || HOSTSFILE=$1
TEMPFILE="/tmp/hosts"
CHECKFILE="/data/terraform/scripts/check_ssh.sh"
for i in {1..30};do
   cat $HOSTSFILE | grep -v '\[' > $TEMPFILE
   bash $CHECKFILE -f $TEMPFILE -uroot -p 22 | grep -q bad
   if [[ $? -eq 0 ]];then
      sleep 20
      continue
   else
      break
   fi
done
rm -f $TEMPFILE

顺带说下资产的生成形式

 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
# inventory.tf
data "template_file" "ansible_inventory" {
  template = file("./templates/a-hosts.tpl")
  vars = {
    active_team = "${join("\n", data.tencentcloud_instances.a.instance_list[*].private_ip)}",
    active_chicken = "${join("\n", data.tencentcloud_instances.b.instance_list[*].private_ip)}",
  }
}
resource "local_file" "save_inventory" {
  count = 1
  content  = data.template_file.ansible_inventory.rendered
  filename = "./ansible/inventory/active-hosts"

  provisioner "local-exec" {
    command = "export ANSIBLE_HOST_KEY_CHECKING=False"
  }
}
data "template_file" "version_inventory" {
  template = file("./templates/version-hosts.tpl")
   vars = {
    a = "${join("\n", [for i in data.tencentcloud_instances.a.instance_list: "${i.instance_id} ${i.instance_name} ${i.public_ip} ${i.private_ip} ${i.image_id}"])}",
    b = "${join("\n", [for i in data.tencentcloud_instances.b.instance_list: "${i.instance_id} ${i.instance_name} ${i.public_ip} ${i.private_ip}  ${i.image_id}"])}",
  }
}
resource "local_file" "save_version_inventory" {
  content  = data.template_file.version_inventory.rendered
  filename = "./ansible/inventory/version-hosts"
}

tpl里面有jinja的格式即可

items advantages disadvantages
two execute hosts only affects different zone with different hosts cygwinssh too slow

发现问题: 依赖ssh,还是需要处理ssh问题,影响发布效率

v4版本

由于严重依赖cygwin ssh,并且存在不稳定和比较慢的情况,通过计划任务+cygwin脚本实现开机即开服 顺便分享一个bat和计划任务设置,让开机即开程序

1
2
3
4
5
6
7
8
# start_xxx.bat
# @echo off
# C:
# chdir C:\cygwin64\bin
# REM bash --login -i
# start mintty.exe c:\cygwin64\bin\bash --login -i -c "bash /home/root/start_xxx.sh > /home/root/start.out"
# 计划任务
#program set:  c:\cygwin64\home\root\start_battle.bat
items advantages disadvantages
image start battle when start up very fast depend on restart for hostname

发现问题: 依赖hostname来确认自己是否是a还是b 解决办法思考:

  1. 短期

需要构建2个镜像,分别是a和b,实现分别的购买开程序操作

  1. 长期

应用自己能区分自己的属性是a还是b 这样整体up时间预计缩短到3分钟以内 有个问题就是我们把业务类型放进了主机名里面,但是cvm创建的时候需要重启才会生效主机名,这个是非常麻烦的

v5

通过win注册表获取即将修改的主机

1
2
3
4
5
6
7
8
win_host=$(reg query "HKLM\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName"|egrep -i '(typea|typeb)'|awk '{print $NF}'|tr 'A-Z' 'a-z'|tr -d '\r')
if [ "${win_host}" == "typea" ];then
   echo do a something
elif [ "${win_host}" == "typeb" ];then
   echo do b something
else
   echo "bad win host ${win_host}" && exit 1
fi

均衡分区、子网、置换群组

通过对分区和子网这个列表,分别进行取余(第几个%总长度再取值),实现均衡划分

1
2
3
4
5
6
大致的样子
#+begin_src sh
your_subnet_count = ceil(local.count_a / 200) * length(var.your_AZs)
strict_team_instance_names = [for i in range(local.count_a):"${var.your_AZs[i%length(var.your_AZs)]}-${floor(i/length(var.your_AZs))}"]
az: var.your_AZs[index(local.your_names, each.value) % length(var.your_AZs)]
subnet: tencentcloud_subnet.your_subnets[index(local.your_names, each.value)%local.your_subnet_count].id

服务器销毁

使用terraform destroy,target指定工作区

1
terraform destroy  -parallelism=100 -target="module.a-zone" -auto-approve

服务器扩缩容

修改对应的count,再次执行purchase即可

1
terraform apply -parallelism=200 -target="module.a-zone" -var="plans=purchase" -auto-approve

灰度逻辑

针对灰度机器,通过ansible入参ABTest即可

crash拉起

pip版supervisord实现

滚动更新逻辑

通过应用锁服务器实现服务器不再分配,这批服务器修改对应的image id,完成销毁再创建过程,terraform入参是 一个完整的大字典

总结

  • terraform在购买一系列基础设施时候是比较方便的,尤其是还需要针对此机器进行额外的操作
  • terraform在均衡子网,均衡az等方面比较方便,但如果有开发技术,通过云厂商的sdk也不难
  • 通过terraform创建的机器,维护起来需要谨小慎微
  • 镜像即版本的发布逻辑在大包的情况比较合适,还有提升的空间
  • 滚动更新目前的逻辑可以实现,对应更新战斗服进程包,实际上通过patch或者发布系统会更好一点
  • ansible能做很多基础任务,比较方便,对于复杂场景,个人还是建议通过脚本或者完善的发布系统来实现并发等