写点什么

GrowingIO Terraform 实践

  • 2021 年 12 月 09 日
  • 本文字数:3614 字

    阅读完需:约 12 分钟

GrowingIO Terraform 实践

背景

为满足 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

项目实践

项目设计

  • 客户项目存在多个同构环境,环境交付需要一致。

  • 每个环境中存在中多个项目,各项目对资源调度需求各异。

  • 每个项目需要使用EC2ELBEBS、 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中,循环主要依赖于countfor_each,这两种方法均只支持简单的循环,而for循环更多的是参与计算,并不会直接在resource中直接进行使用。

如在app1项目中,需要创建 5 台实例,同时实例需分布在不同的subnet之中,但subnet只有 3 个。在该情况下,我们无法简单的以subnetid作循环,更为重要的是,如果后期 subnet的数量也可能会变化,所以无法固定循环列表。

对于复杂的循环需求,一般将其置于locals中作相关计算,其后在resource中进行引用 。

locals中的计算,相关代码如下:

# module/app1/locals.tf...
//case for the rc2 number is more than the number of zone for subnetlocals { 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_combinelist,其后在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,再次对globalmodule作引用。

# module/application/locals.tf...// In order to make global variables --beginningmodule "global" {  source = "../global"}
locals { ... aws_profile_name = module.global.aws_profile_name}// In order to make global variables --end...
复制代码

最后在项目中,作对应自身modulelocals值作相关的引用。

# 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有兴趣的同学可自行学习了解。


参考

  1. Mikael Krief. Terraform Cookbook: Efficiently define, launch, and manage Infrastructure as Code across various cloud platforms. Packt Publishing 2020

  2. Scott Winkler. Terraform in Action. Manning 2021

  3. Yevgeniy Brikman. Terraform: Up & Running: Writing Infrastructure as Code. O'Reilly Media 2019

  4. Terraform


发布于: 2 小时前阅读数: 5
用户头像

GrowingIO 技术团队经验分享 2020.05.09 加入

GrowingIO(官网网站www.growingio.com)的官方技术专栏,内容涵盖微服务架构,前端技术,数据可视化,DevOps,大数据方面的经验分享。 公众号:GrowingIO技术团队

评论

发布
暂无评论
GrowingIO Terraform 实践