int32bit
作者int32bit·2019-10-08 10:07
研发工程师·民生银行

混合云编排工具Terraform简介

字数 13008阅读 9164评论 0赞 2

1 Terraform背景

1.1 混合云编排

目前各大公有云以及云管理平台均提供了非常友好的交互界面,用户可以像超市买东西一样自助采购云资源。然而,当用户需要批量快速打包采购大量不同类型的云资源时,云管页面过多的交互反而降低了资源采购效率。

据统计,一个熟悉AWS页面操作的工程师,在AWS上初始化一个VPC包括创建VPC、子网、internet网关、NAT网关、路由等工作至少需要花费20分钟的时间,如果涉及跨多个云平台,则花费的时间势必会更长。这就像我们逛超市,不同的商品在不同的区域,甚至不同的售卖方式,有些东西还可能需要逛多个超市去买,很多重复的体力活,一件一件地买不仅工作量繁重还非常浪费时间。

如果我们只需要写一份完整货单然后直接下单效率就会提高很多,而且货单可以随时修改和复用。在云计算中这被称作资源编排(Orchestration),其实在很多云平台中都已经提供了资源编排的功能,比如AWS的CloudFormation、OpenStack的Heat等。

但是如上工具通常仅限用于自家的单一云平台上,而在混合云场景下,往往需要跨多个云平台,不仅有IaaS资源,还有PaaS资源,如果杂糅非常多的云编排工具,则不仅导致学习成本高、代码复用率低,还导致管理混乱,不利于多云的协同工作。解决如上问题的方法是引入一个统一的编排工具,能够通过相同的语法同时编排AWS、GCP、Kubernetes、Vmware、OpenStack、阿里云等云资源。

而这个混合云统一编排工具目前已经有很好的开源项目支持了,它就是下面将要介绍的Terraform项目。

1.2 Terraform简介

Terraform的设计目标为Infrastructure as Code,这里的Infrastructure是一个非常抽象的东西,可以认为是数据中心的一切抽象,如二层网络、交换机(子网)、路由器、虚拟机、负载均衡、防火墙、数据库等等。

Terraform是由Hashicorp公司推出的一个开源项目,这是一家牛逼的公司,除了Terraform项目,开源的项目还有我们熟知的Consul、Vault、Nomad等,涵盖了应用的Provision(资源供给)、Secure(密钥安全)、Connect(分布式通信)、Run(运行)4个阶段。

Terraform和前面提到的CloudFormation、Heat相比除了支持混合云统一编排以外,还有个不同之处在于,Terraform在真正执行之前中间会有个plan计划阶段,这个阶段能够预览哪些资源会新创建、哪些资源会被删除以及更新,这有点像git在commit之前先执行diff人工review下代码,让开发者能够提前检查语法是否有错误以及资源是否为期望结果。

可能有人会说目前不是已经有一些配置管理工具(Configuration Management)如Puppet、Ansible、Chef等同样也可以创建云资源,比如Ansible就提供了模块支持创建AWS资源Ansible: Amazon Web Services,甚至可以通过 -- dry - run参数实现类似Terraform的plan功能。

但二者其实是有差别的,首先Orchestration如Terraform主要解决底层基础设施资源管理问题,而配置管理工具如Ansible主要面向操作系统的配置。其次Orchestration通常是声明式的(Declarative),声明式只关心最终的全局结果是否符合期望,如果和声明的不一致,则创建或者修改资源使其匹配最终状态。而配置管理工具通常是面向过程的(Procedural),需要告诉它哪个有,哪个没有,哪一步怎么做,执行时关心的是每个指令而不是最终的全局结果。

打个比方使用Ansible和Terraform都可以实现在AWS上同时创建5个虚拟机,并且都是通过 count = 5变量指定实例数量。

Ansible:

    • ec2: count: 5
  1. image: ami-1
  2. instance_type: t2.micro

Terraform:

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

现在如果需要增加1个虚拟机,Terraform只需要把 count值修改为6即可,因为 count表示为全局的最终结果。但Ansible如果把 count修改为6,则会再创建6个虚拟机,一共11个虚拟机,因此要实现创建6台虚拟机,只能再拷贝一份代码并指定 count为1,非常不灵活。

如果修改AMI镜像,Terraform只需要修改ami参数为新镜像ID即可,Ansible则必须重写模块,并且需要手动删除没用的旧虚拟机。

2 Terraform入门

Terrafrom的安装可参考官方的文档Installing Terraform,安装完后建议先配置子命令自动补全功能:

  1. terraform -install-autocomplete

2.1 Provider

Terraform其中一个最重要的概念为Provider,Provider为后端驱动,类似于Ansible的模块或者驱动,Provider为云平台的插件,换句话说,只需要实现Provider,就可以对接任一云平台。目前大多数云平台的Provider插件均已经实现了,AWS对应的Provider为 aws,阿里云对应的Provider为 alicloud。由于资源必然属于某个云平台,因此显然Terraform中所有的资源必须隶属于某个Provider。

Terraform目前支持超过160多种的providers,可以说只要人听过的云都能支持,主流的如AWS、GCP、OpenStack等,国内的阿里云、腾讯云、Ucloud以及OpenStack系的华为云、京东云等。

除了公有云,私有云如Oracle、Vmware的支持也都完全没有问题。同时也支持目前主流的PaaS平台,如Kubernetes、Helm、Rancher2等,基本不需要再造轮子直接用就完了。

Provider在Terraform中以插件的形式加载,在init阶段Terraform会自动下载所需要的所有Provider插件。

定义Provider实例的语法如下:

  1. provider "aws" {
  2. profile = "default"
  3. region = "cn-northwest-1"
  4. }

语法和Puppet、Ansible非常类似。

其中 aws为需要加载的Provider插件名称,大括号里面的内容为该Provider的配置, profile为 default表示AWS的认证信息为 ~ /.aws/ credentials的 default配置信息。当然也可以直接把AKSK(access key、secret key)直接硬编码放到provider,不过这存在AKSK泄露的隐患,不建议这么做。

如上运行 teraform init会自动下载Provider aws插件。

2.2 Resource

2.2.1 Resource声明与创建

Resource是Terraform的主角,开发者大多数工作都是和Resource打交道,云平台中所有的资源都可以抽象为Terraform中的一个Resource实例。

定义一个Resource的语法非常简单,以官方的demo为例:

  1. cat example.tf

  2. / 省略了Provider的定义 /
  3. resource "aws_instance" "example" {
  4. ami = "ami-0829e595217a759b9"
  5. instance_type = "t2.micro"
  6. tags = {
  7. "Owner" = "int32bit"
  8. "Name" = "int32bit-test-ft"
  9. }
  10. }
  • 其中 aws_instance为资源类型(Resource Type),定义这个资源的类型,告诉Terraform这个Resource是AWS的虚拟机还是阿里云的VPC。
  • example为资源名称(Resource Name),资源名称在同一个模块中必须唯一,主要用于供其他资源引用该资源。
  • 大括号里面的block块为配置参数(Configuration Arguments),定义资源的属性,比如虚拟机的规格、镜像、标签等。

显然这个Terraform脚本的功能为在AWS上创建一个EC2实例,镜像ID为 ami - 0829e595217a759b9,规格为 t2 . micro,自定义了 Owner和 Name两个标签。

运行 terraform init将根据脚本内容自动下载Provider插件:

我们可以随时通过 terraform plan预览查看这个脚本将要执行的任务:

  1. terraform plan

  2. An execution plan has been generated and is shown below.
  3. Resource actions are indicated with the following symbols:
    • create
  4. Terraform will perform the following actions:
  5. aws_instance.example will be created

    • resource "aws_instance" "example" {
    • ami = "ami-0829e595217a759b9"
    • arn = (known after apply)
    • tags = {
    • "Name" = "int32bit-test-ft"
    • "Owner" = "int32bit"
  6. }
    • vpc_security_group_ids = (known after apply)
    • ...
  7. Plan: 1 to add, 0 to change, 0 to destroy.

如上输出可知,Terraform脚本将创建一个资源 aws_instance . example,其中某些属性如ARN为 known after apply,说明需要apply之后才能知道。

最后执行 terrafrom apply执行:

  1. terraform apply

  2. Plan: 1 to add, 0 to change, 0 to destroy.
  3. Do you want to perform these actions?
  4. Terraform will perform the actions described above.
  5. Only 'yes' will be accepted to approve.
  6. Enter a value: yes
  7. aws_instance.example: Creating...
  8. aws_instance.example: Still creating... [10s elapsed]
  9. aws_instance.example: Still creating... [20s elapsed]
  10. aws_instance.example: Creation complete after 20s [id=i-0bb96d24b6e6d37eb]
  11. Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

    apply会自动调用 plan预览将要改变的资源,输入 yes确认无误后真正执行,由输出可知创建的EC2 ID为 i - 0bb96d24b6e6d37eb。

AWS查看虚拟机信息如下:

2.2.2 Resource state文件

通过 terraform show可查看创建的资源列表。

  1. terraform show

  2. aws_instance.example:

  3. resource "aws_instance" "example" {
  4. ami = "ami-0829e595217a759b9"
  5. arn = "arn:aws-cn:ec2:cn-northwest-1:769527305874:instance/i-0bb96d24b6e6d37eb"
  6. availability_zone = "cn-northwest-1b"
  7. id = "i-0bb96d24b6e6d37eb"
  8. instance_state = "running"
  9. instance_type = "t2.micro"
  10. private_ip = "172.31.29.5"
  11. tags = {
  12. "Name" = "int32bit-test-ft"
  13. "Owner" = "int32bit"
  14. }
  15. root_block_device {
  16. delete_on_termination = true
  17. encrypted = false
  18. iops = 100
  19. volume_id = "vol-033ff2804c08b927a"
  20. volume_size = 8
  21. volume_type = "gp2"
  22. }
  23. }

注意 terrafrom show读取的是Terraform自己的数据库而不是调用云平台API,所有Terraform的资源都会保存到自己的数据库上,默认会放在本地目录,文件名为 terraform . tfstate,这个 state文件非常重要,如果该文件损坏将导致已创建的资源被破坏或者重建,因此可以认为Terraform是一个有状态服务,涉及多人协作时不仅需要拷贝代码,还需要拷贝 state文件,这会导致维护起来特别麻烦,可幸的是Terraform支持把 state文件放到S3上或者consul,参考官方文档Remote State,建议把state文件从代码中分离放到S3上。

2.2.3 Resource更新

由Terraform的Infrastructure as Code的设计目标,资源是可以随时修改的,如下EC2增加一个标签 Newkey:

  1. resource "aws_instance" "example" {
  2. ami = "ami-0829e595217a759b9"
  3. instance_type = "t2.micro"
  4. tags = {
  5. "Owner" = "int32bit"
  6. "Name" = "int32bit-test-ft"
  7. "Newkey" = "test_new_key"
  8. }
  9. }

这里省略plan步骤直接apply:

  1. terraform apply

  2. aws_instance.example: Refreshing state... [id=i-0bb96d24b6e6d37eb]
  3. An execution plan has been generated and is shown below.
  4. Resource actions are indicated with the following symbols:
  5. ~ update in-place
  6. Terraform will perform the following actions:
  7. aws_instance.example will be updated in-place

  8. ~ resource "aws_instance" "example" {
  9. / ... /
  10. ~ tags = {
  11. "Name" = "int32bit-test-ft"
    • "Newkey" = "test_new_key"
  12. "Owner" = "int32bit"
  13. }
  14. }
  15. Plan: 0 to add, 1 to change, 0 to destroy.
  16. aws_instance.example: Modifying... [id=i-0bb96d24b6e6d37eb]
  17. aws_instance.example: Modifications complete after 1s [id=i-0bb96d24b6e6d37eb]
  18. Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

如上 update in - place表示不需要重建资源直接修改资源属性,由于本次修改只是添加一个标签,不需要重新创建虚拟机,因此可以通过 update in - place实现

资源预览中 +表示添加的内容, ~表示更新的内容, -表示即将删除的内容。

如果修改 AMI如下:

  1. resource "aws_instance" "example" {
  2. ami = "ami-08af324f69cf03287"
  3. instance_type = "t2.micro"
  4. tags = {
  5. "Owner" = "int32bit"
  6. "Name" = "int32bit-test-ft"
  7. "Newkey" = "test_new_key"
  8. }
  9. }

执行 apply结果如下:

  1. terraform apply

  2. aws_instance.example: Refreshing state... [id=i-0bb96d24b6e6d37eb]
  3. Resource actions are indicated with the following symbols:
  4. -/+ destroy and then create replacement
  5. Terraform will perform the following actions:
  6. aws_instance.example must be replaced

  7. -/+ resource "aws_instance" "example" {
  8. ~ ami = "ami-0829e595217a759b9" -> "ami-08af324f69cf03287"
  9. ~ id = "i-0bb96d24b6e6d37eb" -> (known after apply)
  10. ~ instance_state = "running" -> (known after apply)
  11. ~ private_ip = "172.31.29.5" -> (known after apply)
  12. tags = {
  13. "Name" = "int32bit-test-ft"
  14. "Newkey" = "test_new_key"
  15. "Owner" = "int32bit"
  16. }
  17. ~ root_block_device {
  18. ~ delete_on_termination = true -> (known after apply)
  19. ~ encrypted = false -> (known after apply)
  20. ~ iops = 100 -> (known after apply)
    • kms_key_id = (known after apply)
  21. ~ volume_id = "vol-033ff2804c08b927a" -> (known after apply)
  22. ~ volume_size = 8 -> (known after apply)
  23. ~ volume_type = "gp2" -> (known after apply)
  24. }
  25. }
  26. Plan: 1 to add, 0 to change, 1 to destroy.
  27. aws_instance.example: Destroying... [id=i-0bb96d24b6e6d37eb]
  28. aws_instance.example: Destruction complete after 30s
  29. aws_instance.example: Creating...
  30. aws_instance.example: Creation complete after 41s [id=i-0f87444adc1c2b7b4]
  31. Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

此时由于AWS EC2实例不支持直接修改AMI,因此Terraform为了与我们的声明最终期望一致,先把之前的资源删除,然后创建一个新的EC2实例。

从AWS console上也可以看到原来的虚拟机 terminate了,重新创建了一个新的虚拟机:

删除的资源不可回退,因此建议在 apply之前在plan中仔细查看哪些资源是原地修改的,哪些需要重建,哪些资源会被删除,避免资源被意外删除。

2.2.4 Resource之间的依赖

和Ansible、Puppet一样资源之间可能会有依赖,Terraform支持隐式依赖和显式依赖,隐式依赖由Terraform自动根据资源的引用关系分析资源的依赖关系,比如A引用了B,则A依赖于B,A资源创建之前必须先创建B。

如下我们为EC2实例绑定个弹性IP:

  1. resource "aws_instance" "example" {
  2. ami = "ami-08af324f69cf03287"
  3. instance_type = "t2.micro"
  4. tags = {
  5. "Owner" = "int32bit"
  6. "Name" = "int32bit-test-ft"
  7. "Newkey" = "test_new_key"
  8. }
  9. }
  10. resource "aws_eip" "example_public_ip" {
  11. vpc = true
  12. instance = aws_instance.example.id
  13. }

此时由于 example_public_ip引用了 example的 id,因此 example_public_ip依赖于EC2实例 example,Terraform会先创建EC2实例,然后绑定弹性IP。

隐式依赖基本能解决90%的问题,大多数情况下我们不需要显式告诉Terraform哪些资源存在依赖。当然仍可能存在需要显式依赖的情况,Terraform通过 depends_on指明资源所依赖的资源列表,比如EC2实例需要通过role访问S3,此时需要在虚拟机中关联角色,角色中的policy必须先就绪,这种情况下资源之间没有引用关系,Terraform无法推导资源的依赖关系,因此必须通过 depends_on显示声明所依赖的资源。

  1. depends_on = [
  2. aws_iam_role_policy.example,
  3. ]

通过Terraform的 graph可以导出资源的依赖图:

  1. terraform graph | dot -Tsvg > graph.svg

3 Terraform其他功能

3.1 Input Variables

如上实例把AMI和instance type硬编码到脚本中非常不灵活,Terraform支持输入变量功能,建议把变量单独抽取出来,变量通过 variable关键字声明:

  1. variable "image_id" {
  2. type = string
  3. default = "ami-08af324f69cf03287"
  4. description = "The id of the machine image (AMI) to use for the server."
  5. }
  6. variable "instance_type" {
  7. default = "t2.micro"
  8. }

变量包括变量名称以及数据类型,数据类型默认为 string,另外可以提供default默认值以及description。

此时可以在同一模块中的任意Resource通过 var .变量名引用变量:

  1. resource "aws_instance" "example" {
  2. ami = var.image_id
  3. instance_type = var.instance_type
  4. tags = {
  5. "Owner" = "int32bit"
  6. "Name" = "int32bit-test-ft"
  7. "Newkey" = "test_new_key"
  8. }
  9. }

此时 plan以及 apply均可以通过形如 - var a = b的形式指定变量值:

  1. terraform apply \
  2. -var instance_type=t2.small \
  3. -var image_id=ami-0fcb508ec48b146df

也可以通过后缀名为 . tfvars的文件指定变量值:

  1. cat example.tfvars

  2. image_id = "ami-0fcb508ec48b146df"
  3. instance_type = "t2.small"
  4. terraform apply -var-file=example.tfvars

如果tfvars文件名为 terraform . tfvars或者 *. auto . tfvars,则Terraform会自动加载不需要通过 - var - file指定。

另外还可以通过环境变量的形式指定变量值,环境变量名为 TF_VAR_name,如 TF_VAR_image_id。

如果变量没有指定并且没有默认值,则在apply时会通过交互方式请求用户手动输入变量值。

3.2 Output Values

Output values用于Terraform执行完后输出结果,在多模块中子模块的output还可以被父模块引用。

如下输出EC2实例的ID以及私网IP:

  1. output "instance_id" {
  2. value = aws_instance.example.id
  3. }
  4. output "private_ip" {
  5. value = aws_instance.example.private_ip
  6. }

再次执行 apply:

  1. terraform apply

  2. aws_instance.example: Refreshing state... [id=i-0f87444adc1c2b7b4]
  3. aws_eip.ip: Refreshing state... [id=eipalloc-0e2cec51cbf18b5d3]
  4. Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
  5. Outputs:
  6. instance_id = i-0f87444adc1c2b7b4
  7. private_ip = 172.31.19.113

也可以通过 terraform output命令输出output值:

  1. terraform output

  2. instance_ip = i-0f87444adc1c2b7b4
  3. private_ip = 172.31.19.113

3.3 Modules

如果把所有的资源都杂糅放到一块,则必然导致脚本很难维护,因此有必要按照不同的功能将代码分开,Terraform支持Module功能,通过 source加载子模块。

如下是目录的结构:

  1. tree

  2. .
  3. ├── example.tfvars
  4. ├── main.tf
  5. ├── sub_module
  6. │ ├── main.tf
  7. │ └── variables.tf
  8. ├── terraform.tfstate
  9. └── terraform.tfstate.backup
  10. 1 directory, 6 files

在当前工作目录中有一个子目录sub_module,这个子目录也是一个基本完整的Terraform项目。然后通过 module关键字加载模块:

  1. cat main.tf

  2. provider "aws" {
  3. profile = "default"
  4. region = "cn-northwest-1"
  5. }
  6. module "example" {
  7. source = "./sub_module"
  8. image_id = "ami-08af324f69cf03287"
  9. instance_type = "t2.micro"
  10. }
  11. output "private_ip" {
  12. value = module.example.private_ip
  13. }
  14. output "instance_id" {
  15. value = module.example.instance_id
  16. }

module中必须通过 source参数指定子模块路径,如果子模块中有Input Variables,可以在module的body中指定。

另外由上面的例子可知,在父模块中可以引用子模块的output值。

3.4 Data Sources

前面我们使用的参数都是固定的静态变量,但有些情况下可能参数变量不确定或者参数可能随时变化。比如我们创建虚拟机通常需要指定我们自己的镜像模块,但我们的模板可能随时更新,如果在代码中指定AMI ID,则一旦我们更新镜像模板就需要重新修改代码。

Terraform中的Data Sources可以认为是动态变量,只有在运行时才能知道变量的值。

Data Sources通过 data关键字声明,如下:

  1. data "aws_ami" "my_image" {
  2. most_recent = true
  3. owners = ["self"]
  4. tags = {
  5. Name = "test-template-ami"
  6. Tested = "True"
  7. }
  8. filter {
  9. name = "state"
  10. values = ["available"]
  11. }
  12. }
  13. resource "aws_instance" "example" {
  14. ami = data.aws_ami.my_image.id
  15. instance_type = var.instance_type
  16. tags = {
  17. "Owner" = "int32bit"
  18. "Name" = "int32bit-test-ft"
  19. "Newkey" = "test_new_key"
  20. }
  21. }

如上例子中的EC2镜像没有指定AMI ID,而是通过 data引用,Terraform运行时将首先根据标签选择镜像,然后选择状态为 available的镜像,如果同时有多个镜像满足条件,则选择最新的镜像。

4 总结

Terraform是非常强大的混合云编排工具,语法简单明了,只需要通过配置文件声明需要的资源列表,Terraform就能够快速地完成多云资源的创建。

当然Terraform也有个问题就是前面提到的它是一个有状态服务,意味着被Terraform管理的资源,不能通过手动或者借助其他工具管理资源,因为外部修改资源后,Terraform会认为和期望结果不一致而触发一次更新操作。

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

2

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广