背景
为满足 GrowingIO 客户多样性的需求,在公有云设施上使用 Terraform 作资源管理。采取 Terrform 具有以下相关优势:
- 多云支持,主流云厂商均提供对应的
Provider
支持。 - 自动化管理基础结构,可重复对资源进行编排使用。
- 基础架构即代码
(Infrastructure as Code)
,允许保存基础设施状态,便于追踪管理。 - 统一的语法来管理不同的云服务,实现标准化管理。
Terraform 介绍
概念
Terraform
是一个开源 IAS
工具,提供一致的 CLI
工作流,可管理数百个云服务。 Terraform
通过将云厂商提供的 API
编写为声明式配置文件,通过Terraform
的命令行接口,可将资源调度配置应用到任意支持的云上,并实现版本控制。更多详情请参见HashiCorp Terraform。
Terraform
通过不同的Provider
来支持不同云平台。国外云服务商如 Azure
, AWS
, GoogleCloud
, DigtalOcean
,国内云服务商如 Aliyun
, TencentCloud
, Ucloud
, BaiduCloud
均有提供官方的 Provider
。
架构
Terraform
通过解析用户书写的HCL(HashiCorp Configuration Language)
格式的DSL
文件,然后通过Terraform core
与各云厂商提供的Providers
进行交互,从而进行相关资源的调度。各云厂商依HCL
代码风格,将自家资源调用API
重新封装,以生成对应的 Providers
。
项目实践
项目设计
- 客户项目存在多个同构环境,环境交付需要一致。
- 每个环境中存在中多个项目,各项目对资源调度需求各异。
- 每个项目需要使用
EC2
、ELB
、EBS
、EMR
等多种资源。
项目实现
项目结构
# tree -L 3 .
├── README.MD
├── module
│ ├── app1
│ │ ├── config.tf
│ │ ├── locals.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── global
│ │ └── config.tf
└── dev
├── main.tf
└── output.tf
目录结构拆解:
module
中依项目名进行封装。其中app1
为项目名。global
为全局参数设置。dev
为各环境对module
的引用封装。
module
细节:
config.tf
为配置的相关参数,如Providers
的相关参数设定。locals.tf
为变量的计算生成与其它变量引用,如module
的引用与一些复杂变量的重生成。main.tf
为resource
与data
相关的资源编排调用。outputs.tf
为项目输出值,如创建ecs
之后主机的ip
等。variables.tf
为传参的相关设置,如需创建ecs
的数量。
module
封装
module
的资源引用
在对资源调度的实施过程中,往往需要多次重复作业,故需将多个原子操作,统一封装成module
,后续在外部引用module
并传入对应参数即可。
apply
构建文件代码:
# dev/main.tf
...
module "app1" {
source = "../module/app1"
aws_ec2_create_number = 3
...
}
module
的条件判断
在 Terraform
中,往往将循环与判断结合使用,主要使用场景有两种:
- 确认变量形式
- 确认资源是否创建
如创建 ecs
时的主机名设置,当创建多台主机时,主机名需数字后缀以区分,而只有一台主机时,不需要数字后缀。
相关实例代码如下:
# module/app1/main.tf
...
resource "aws_instance" "aws-ec2-create" {
count = var.aws_ec2_create_number
...
tags = {
Name = var.aws_ec2_create_number < 2 ? "${var.env}-${var.aws_ec2_name}" : "${var.env}-${var.aws_ec2_name}-${count.index + 1}"
...
}
...
如在ECS
的创建之中,无法判断用户是否需要数据盘。
相关实例代码如下:
# module/app1/main.tf
...
resource "aws_instance" "aws-ec2-create" {
count = var.aws_ec2_create_number
...
dynamic "ebs_block_device" {
for_each = var.aws_ebs_block_device_volume_size != 0 ? [1] : []
content {
delete_on_termination = true
device_name = var.aws_ebs_block_device_name
volume_size = var.aws_ebs_block_device_volume_size
volume_type = var.aws_ebs_block_device_volume_type
}
}
...
}
...
当用户传入var.aws_ebs_block_device_volume_size
的值为0
时,即循环一个空列表,即不创建该资源,亦即不创建数据盘。
module
的复杂循环
在 Terraform
中,循环主要依赖于count
与for_each
,这两种方法均只支持简单的循环,而for
循环更多的是参与计算,并不会直接在resource
中直接进行使用。
如在app1
项目中,需要创建5台实例,同时实例需分布在不同的subnet
之中,但subnet
只有3个。在该情况下,我们无法简单的以subnet
的id
作循环,更为重要的是,如果后期 subnet
的数量也可能会变化,所以无法固定循环列表。
对于复杂的循环需求,一般将其置于locals
中作相关计算,其后在resource
中进行引用 。
在locals
中的计算,相关代码如下:
# module/app1/locals.tf
...
//case for the rc2 number is more than the number of zone for subnet
locals {
times = ceil(var.aws_ec2_create_number / length(var.aws_subnet_id_list))
}
locals {
// loop two list to generate a new list
subnet_list_combine = flatten([
for p in range(local.times) : [
for q in var.aws_subnet_id_list : [join(",", [q])]
]
]
)
}
...
通过在locals
中的计算,我们可以得到一个名为subnet_list_combine
的list
,其后在resource
中进行引用即可。
resource
相关代码如下:
# module/app1/main.tf
...
resource "aws_network_interface" "aws-network-interface" {
count = var.aws_ec2_create_number
subnet_id = local.subnet_list_combine[count.index]
...
全局变量
在Terraform
中,官方为了层级的简洁,默认不推荐使用全局变量,因为全局变量的设置,会出现所见非所得的现象,详见Terraform global variables
但在实际生产中,却有相关需求,如 aws_profile_name
在每个项目中均一致,同时后期因为用户的 profile
设置不一致而需要统一变更。
我们可以将此类参数写入一个 module
之中。
# module/global/config.tf
...
output "aws_profile_name" {
value = "default"
}
...
在各项目中的module
,再次对global
的module
作引用。
# module/application/locals.tf
...
// In order to make global variables --beginning
module "global" {
source = "../global"
}
locals {
...
aws_profile_name = module.global.aws_profile_name
}
// In order to make global variables --end
...
最后在项目中,作对应自身module
的locals
值作相关的引用。
# module/application/config.tf
...
Provider "aws" {
profile = var.aws_profile_name == "" ? local.aws_profile_name : var.aws_profile_name
...
}
...
环境隔离
在Terraform
中,隔离一般有两种:
workspace
隔离- 目录隔离
在 workspace
隔离中,需要使用 terraform workspace
子命令,与 git branch
类似,但是 terraform workspace
中的隔离,并不直观,在生产中容易出现误操作,所以对于不同环境的 module
调用,本项目中采用了目录隔离。
# dev/main.tf
...
module "app1" {
source = "../module/app1"
aws_ec2_create_number = 3
...
}
# stage/main.tf
...
module "app1" {
source = "../module/app1"
aws_ec2_create_number = 3
...
}
项目心得
Terraform
在基础资源编排中,使用方便,语法简洁,但由于各云厂商提供 Provider
的风格并不完全统一,一定程度上增加了多云混合使用的成本。特别是对于国内非 AWS
用户而言,国内部分云厂商提供的Provider
,支持的资源种类相较于 AWS
偏少,部分场景可能无法实现。
同时也因为 Terraform
的语法对于一些高级特性的支持欠缺,导致在部分复杂的场景中,有些捉襟见肘,而更多需要 Provider
去提供对应功能。虽然有 module
的设计,可以进行代码复用,但也有因部分参数无法动态区分,而不得不创建多个 module
以区分,这点在国内云厂商提供的 Provider
中尤为明显。
小结
本文简单介绍了Terraform
的基本概念以及采用Terraform
的原由。
同时例举了在生产实践中Terraform
的目录结构编排与环境隔离,详细说明如何通过传参来动态调整资源编排的实际传参与调度,如何通过多次组合计算以动态生成新的参数来规避Terraform
对高级特性支持的欠缺,以及如何构建全局变量以解决全局动态传参。基于篇幅限制, Terraform
的使用无法逐一说明,对Terraform
有兴趣的同学可自行学习了解。
参考
- Mikael Krief. Terraform Cookbook: Efficiently define, launch, and manage Infrastructure as Code across various cloud platforms. Packt Publishing 2020
- Scott Winkler. Terraform in Action. Manning 2021
- Yevgeniy Brikman. Terraform: Up & Running: Writing Infrastructure as Code. O'Reilly Media 2019
- Terraform