Terraform基礎入門 (Infrastructure as Code)

2023-02-19 06:00:39

前言

像寫程式碼一樣管理基礎設施。

Terraform 使用較為高階的組態檔語法來描述基礎設施,這個特性讓你對組態檔進行版本化管理後,就等於對生產環境的基礎設施進行類似於程式碼一樣的版本化管理,而且這些基礎設施的組態檔可以複用或者分享。

介紹

Terraform(https://www.terraform.io/)是 HashiCorp 旗下的一款開源(Go 語言開發)的 DevOps 基礎架構資源管理運維工具。他的本質是基於版本化的管理能力上,安全、高效地建立和修改使用者生產環境的基礎設施。

Terraform 有很多非常強大的特性值得我們參考:

  • 基礎設施即程式碼:Infrastructure as Code。基礎設施可以使用高階設定語法進行描述,使得基礎設施能夠被程式碼化和版本化,從而可以進行共用和重複使用。
  • 執行計劃:Execution Plans。Terraform有一個 "計劃 "步驟,在這個步驟中,它會生成一個執行計劃。執行計劃顯示了當你呼叫apply時,Terraform會做什麼,這讓你在Terraform操作基礎設施時避免任何意外。
  • 資源圖譜:Resource Graph。Terraform建立了一個所有資源的圖,並行建立和修改任何非依賴性資源。從而使得Terraform可以儘可能高效地構建基礎設施,操作人員可以深入瞭解基礎設施中的依賴性。
  • 自動化變更:Change Automation。複雜的變更集可以應用於您的基礎設施,而只需最少的人工干預。有了前面提到的執行計劃和資源圖,你就可以準確地知道Terraform將改變什麼,以及改變的順序,從而避免了許多可能的人為錯誤。

Terraform 術語

術語 基本介紹
Provider 又稱為Plugin,主要用來跟其他的服務進行互動從而實現資源管理,服務安裝等
Module Module是一個將多種資源整合到一起的一個容器,一個module由一些列的.tf或者.tf.json字尾檔案組成
Resource 主要用來定義各種資源或者服務,而這些服務就組成了我們的基礎設施架構
Registry Provider倉庫,主要用來儲存各種的provider,同時我們也會從Registry下載本地定義的provider到本地

Terraform 如何工作

Terraform 採用了外掛模式的執行機制。Terraform 使用 RPC(遠端介面呼叫) 跟 Terraform Plugins 進行通訊,同時也提供了多種方式來發現和載入 Plugins;而 Terraform Plugins 會和具體的 Provider 進行對接,例如:AWS、Kubernates、Azure 等等,封裝各種資源操作的介面供 Terraform Core 使用。

Terraform使用的是HashiCorp自研的go-plugin庫(https://github.com/hashicorp/go-plugin),本質上各個Provider外掛都是獨立的程序,與Terraform程序之間通過rpc進行呼叫。Terraform引擎首先讀取並分析使用者編寫的Terraform程式碼,形成一個由data與resource組成的圖(Graph),再通過rpc呼叫這些data與resource所對應的Provider外掛;Provider外掛的編寫者根據Terraform所制定的外掛框架來定義各種data和resource,並實現相應的CRUD方法;在實現這些CRUD方法時,可以呼叫目標平臺提供的SDK,或是直接通過呼叫Http(s) API來操作目標平臺。

關於provider

預設情況下Terraform從官方Provider Registry下載安裝Provider外掛。Provider在Registry中的原始地址採用類似registry.terraform.io/hashicorp/aws 的編碼規則。通常為了簡便,Terraform允許省略地址中的主機名部分registry.terraform.io,所以我們可以直接使用地址hashicorp/aws。

有時無法直接從官方Registry下載外掛,例如我們要在一個與公網隔離的環境中執行Terraform時。為了允許Terraform工作在這樣的環境下,有一些可選方法允許我們從其他地方獲取Provider外掛。

安裝

Terraform是以二進位制可執行檔案發布,只需下載terraform,然後將terraform可執行檔案所在目錄新增到系統環境變數PATH中即可。

登入Terraform官網,下載對應作業系統的安裝包。
解壓安裝包,並將terraform可執行檔案所在目錄新增到系統環境變數PATH中。
在命令列中執行如下命令驗證設定路徑是否正確。

terraform version

開啟本地快取

有的時候下載某些Provider會非常緩慢,或是在開發環境中存在許多的Terraform專案,每個專案都保有自己獨立的外掛資料夾非常浪費磁碟,這時我們可以使用外掛快取。

Windows下是在相關使用者的%APPDATA%目錄(如C:\Users\Administrator\AppData\Roaming)下建立名為"terraform.rc"的檔案,Macos和Linux使用者則是在使用者的home下建立名為".terraformrc"的檔案。在檔案中設定如下:

plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"

當啟用外掛快取之後,每當執行terraform init命令時,Terraform引擎會首先檢查期望使用的外掛在快取資料夾中是否已經存在,如果存在,那麼就會將快取的外掛拷貝到當前工作目錄下的.terraform資料夾內。如果外掛不存在,那麼Terraform仍然會像之前那樣下載外掛,並首先儲存在外掛資料夾中,隨後再從外掛資料夾拷貝到當前工作目錄下的.terraform資料夾內。為了儘量避免同一份外掛被儲存多次,只要作業系統提供支援,Terraform就會使用符號連線而不是實際從外掛快取目錄拷貝到工作目錄。

需要特別注意的是,Windows 系統下plugin_cache_dir的路徑也必須使用/作為分隔符,應使用C:/somefolder/plugin_cahce而不是C:\somefolder\plugin_cache

demo1(docker+nginx)

可以用阿里雲等做demo,但云資源一次執行要好幾分鐘,而且很多操作都是要收費的,學習的時候很不方便,就在本地用docker進行學習吧,第一次下載provider慢一點,後面就是幾秒鐘就可以測試一個例子了。

所以看下面的demo,需要先下載並安裝docker。可以使用菜鳥教學

demo來自官方,但做了適當的簡化。
使用docker,並在裡面安裝一個nginx。

provider 的使用檔案,見 https://registry.terraform.io/browse/providers

新建一個檔案,檔名隨便,就叫main.tf,內容如下

terraform {
    required_providers {
        // 宣告要用的provider
        docker = {
            source = "kreuzwerker/docker"
            // 要用的版本,不寫預設拉取最新的
            //version = "~> 2.13.0"
        }
    }
}

// 語法 resource resource_type name
// 宣告docker_image的映象,nginx只是一個名字,只要符合變數命名規則即可
resource "docker_image" "nginx" {
		// 使用的是映象名,https://hub.docker.com/layers/nginx/library/nginx/latest/images/sha256-3536d368b898eef291fb1f6d184a95f8bc1a6f863c48457395aab859fda354d1?context=explore
		name = "nginx:latest"
}

resource "docker_container" "nginx" {
	image = docker_image.nginx.name
	name	= "nginx_test"
	ports {
		internal = 80       // docker 內部的埠
		external = 8000     // 對外存取的埠
	}
	volumes {
		container_path = "/usr/share/nginx/html"
		host_path	   = "/Users/jasper/data/nginx_home"
	}
}

新建目錄/Users/jasper/data/nginx_home,windows去VirtualBox那操作,裡面新建一個檔案index.html,內容隨便來點,就「Hello Terraform」吧。

terraform init

下載需要的provider,第一次可能有點慢

terraform plan

檢視執行計劃

terraform apply

輸入yes即可

最後就可以在瀏覽器進行存取了
http://127.0.0.1:8000/index.html

demo2(docker+zookeeper+kafka)

kafka的安裝是需要依賴zookeeper的,這個例子就展示terraform是如何處理依賴的,並且展示在多資源的情況下,模組編寫的推薦方式。

下面顯示了一個遵循標準結構的模組的完整範例。這個例子包含了所有可選的元素。

$ tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│	 ├── nestedA/
│	 │	 ├── README.md
│	 │	 ├── variables.tf
│	 │	 ├── main.tf
│	 │	 ├── outputs.tf
│	 ├── nestedB/
│	 ├── .../
├── examples/
│	 ├── exampleA/
│	 │	 ├── main.tf
│	 ├── exampleB/
│	 ├── .../
  • README檔案用來描述模組的用途。
  • examples資料夾用來給出一個呼叫樣例(可選)
  • variables.tf檔案,包含模組所有的輸入變數。輸入變數應該有明確的描述說明用途
  • outputs.tf檔案,包含模組所有的輸出值。輸出值應該有明確的描述說明用途
  • modules子目錄,嵌入模組資料夾,出於封裝複雜性或是複用程式碼的目的,我們可以在modules子目錄下建立一些嵌入模組。
  • main.tf,它是模組主要的入口點。對於一個簡單的模組來說,可以把所有資源都定義在裡面;如果是一個比較複雜的模組,我們可以把建立的資源分佈到不同的程式碼檔案中,但參照嵌入模組的程式碼還是應保留在main.tf裡

程式碼見:https://gitee.com/zhongxianyao/terraform-demo/tree/master/demo-zk-kafka

main.tf

terraform {
	required_providers {
		docker = {
			source = "kreuzwerker/docker"
		}
	}
}

module "zookeeper" {
    // 一個本地路徑必須以./或者../為字首來標明要使用的本地路徑,以區別於使用Terraform Registry路徑。
    source = "./modules/zk"
}

module "kafka" {
	source = "./modules/kafka"
	// 由於依賴了zk模組是輸出引數,terraform能夠分析出來依賴關係,這裡無需depends_on引數
	# depends_on = [
	#	 module.zookeeper
	# ]
	zk_port = module.zookeeper.zk_port
}

modules/zk/main.tf

terraform {
	required_providers {
		docker = {
			source = "kreuzwerker/docker"
		}
	}
}

resource "docker_image" "zookeeper" {
	name = "ubuntu/zookeeper:latest"
}

resource "docker_container" "zookeeper" {
	image = docker_image.zookeeper.name
	name	= "zookeeper_test"
	ports {
		internal = 2181
		external = 12181
	}
}

modules/zk/outputs.tf

output "zk_port" {
		// 在表示式中參照資源屬性的語法是<RESOURCE TYPE>.<NAME>.<ATTRIBUTE>。
		value = docker_container.zookeeper.ports[0].external
}

modules/kafka/main.tf

terraform {
	required_providers {
		docker = {
			source = "kreuzwerker/docker"
		}
	}
}

resource "docker_image" "kafka" {
	name = "ubuntu/kafka:latest"
}

resource "docker_container" "kafka" {
	image = docker_image.kafka.name
	name	= "kafka_test"
	// zk的埠傳遞
	env = [
		"ZOOKEEPER_PORT=${var.zk_port}"
	]
	ports {
		internal = 9092
		external = 19092
	}
}

modules/kafka/inputs.tf

variable "zk_port" {
	default = 2181
	description = "zookeeper的埠"
}

最後terraform命令

terraform init
terraform plan
terraform apply

參考資料