博客 / 詳情

返回

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。

項目實踐

項目設計

客户項目存在多個同構環境,環境交付需要一致。
每個環境中存在中多個項目,各項目對資源調度需求各異。
每個項目需要使用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有興趣的同學可自行學習瞭解。

參考

  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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.