背景
為滿足 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