terraform_logo

Introduction to Terraform

Terraform 是一款 Infrastructure as Code (IaC) 工具,以 code 來 定義 / 建造 (build) / 修改 (change) / 版本控制 (version) 基礎設施,支援地端 (on-prem) 與 雲端 (Cloud)。

安裝 Terraform CLI

Install Terraform

1
2
3
4
5
6
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform

## Auto Completion
terraform -install-autocomplete
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
## show version
terraform --version

Terraform v1.0.7
on linux_amd64

## The auto completion file in bashrc
tail ~/.bashrc

...
complete -C /usr/bin/terraform terraform

Terraform Hello World using Docker provider

Build Infrastructure

這邊會使用 Docker 來跟 Terraform 說 Hello~

建立工作目錄

Terraform 設定檔 (configuration) 必須被放在自己的工作目錄下 (working directory)

1
mkdir -p ~/terraform/hello_world

main.tf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
      version = "~> 2.13.0"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.latest
  name  = "tutorial"
  ports {
    internal = 80
    external = 8000
  }
}

terraform Block 解釋

terraform {} 區塊設定 Terraform 相關設定,包含所需要的 provider。 各個 provider 下面的 source 定義 Terraform 要從 Terraform Registry 安裝。
例如範例的 kreuzwerker/docker 就等同於 registry.terraform.io/kreuzwerker/docker

另外 version 雖然是 optional,不過官方建議加上,藉此約束 provider 的版本。 如果使用者沒有定義,在 init 階段 Terraform 會自動下載最新版 (most recent version)。

provider Block 解釋

1
2
3
4
5
provider "aws" {
  access_key = "ACCESS_KEY_HERE"
  secret_key = "SECRET_KEY_HERE"
  region     = "ap-northeast-1"
}

provider 區塊定義特定 provider 的參數,例如: AWS 的相關 token
provider 可以把他當作 Terraform 的 plug-in,Terraform 利用這些 plug-in 去管理資源。

terraform_hello_world_0

▲ 當工作目錄下只有 .tf (主設定檔) 而尚未進行 terraform init 時,執行 terraform validate 會出現 Error。
並且告知 Plugins are external binaries that Terraform uses to access and manipulate resources.
plug-in 是外部二進制檔案

resource Block 解釋

resource 格式:

> 其中 <resource_type> 前綴會帶有 prodvider 的字樣。
> <resource_type> + <resource_name> 會形成一個獨特的 ID,以下面這個例子就是 aws_instance.app_server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# resource <resource_type> <resource_name>

resource "aws_instance" "app_server" {
  ami           = "ami-830c94e3"
  instance_type = "t2.micro"

  tags = {
    Name = "ExampleAppServerInstance"
  }
}

resource 是定義 provider 提供的 「元件」(components) 的地方,可以是實體的 (physical) 或著虛擬的 (virtual)。

Initialize the directory 初始化目錄

Initialize the directory

terraform init 有點類似 gcc 編譯,執行後做了這兩件事情:

  1. 下載/安裝 .tf 內定義的 provider$(pwd)/.terraform 裡面。
  2. 創建一個 .terraform.lock.hcl lock file,裡面定義正在使用的 provider 版本。

terraform_hello_world_1

.terraform.lock.hcl 檔案內容 不要手動去更改!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/kreuzwerker/docker" {
  version     = "2.13.0"
  constraints = "~> 2.13.0"
  hashes = [
    "h1:3r/gPhfPCl4mxazpfg0S/qgxmt+QWuvYT3CXTxUz9fs=",
    "zh:0df685adc7b5740ae0def7235a44e1bce2f71beaf155319c2464ad2fba5cb321",
    "zh:2cf4b4f840fa84f1b906f4cca58c9782375e9988ad354afcd85b0180cd784205",
    "zh:347b189655afdc0df1919a26fb64cb745bb02d8fa2006a087cb6679a1b62319d",
    "zh:441521c85fecad348ca012db7b9d14544cbe0a237012f8a03d5660c73e9a32a6",
    "zh:462a1f67d26182fbb5ee78bb8d4764a2983804fa5f9971ca006da439e9e97055",
    "zh:53822eb743cd487cabbed3360221cc0404b80f933b746d80426a4e10fa2f958a",
    "zh:55c6eda01dd3d3f877aad16de6bf91e84bfa9c93f852869581429640be19d472",
    "zh:690bb327398f800f7945bab35b1ad2c6ec1c0fa7f8a1e5696b0bc4597540e3af",
    "zh:6c55a9a761596ca974a9cbaeee3179fb8f50916fad18d2422a2d818c3f4dc241",
    "zh:6efd9e6ffa4c4c73fd39c856456022aad6a3a0b176c550409345e894475bbf4f",
    "zh:811a37e3a66d5e99a81e0e66c817363205b030962fcec68bb96ab53b029ffeac",
    "zh:aacb4ab8dd11e834952877390bc19beabf9fb0591c101e96da559201f4b284ca",
    "zh:cecdf49f9488a10ac9416be354e7de3ed45114a25235cebc4ec6771696d980e9",
  ]
}

Format and validate the configuration 正規化/驗證 設定檔

Format and validate the configuration

1
terraform fmt

.tf 正規化 (formating),使設定檔可讀性更高、更嚴謹。只有被更動的設定檔會被顯示出來

1
2
3
terraform validate

Success! The configuration is valid.

▲ 驗證語法

Create infrastructure 創建基礎設施

Create infrastructure

在執行 terraform apply並不會馬上執行 ,Terraform 會先列出 「執行清單」(execution plan)
輸出內容與 git diff 頗為相似, + 項目會被 Terraform 創建。
known after apply 代表這個數值將在物件建立後才能取得。

terraform_hello_world_2

▲ 輸入 yes 才會開始執行。

terraform_hello_world_3

▲ 建立成功!

terraform_hello_world_4

▲ 工作目錄下多出 terraform.tfstate,裡面儲存 infra 的狀態。

Inspect state 查看狀態

Inspect state

terraform.tfstate 是 Terraform 唯一 追蹤由它建立的 resource 的方式,透過這個檔案去執行 update 或者 destory
另外官方提醒 terraform.tfstate 通常含有 機敏資訊 sensitive information 建議儲存在 Terraform Cloud 或者 Terraform Enterprise (還敢業配阿),我在 Docker 這個範例當中是沒有找到什麼啦~ 之後使用 vsphere provider 的時候再來看看裡面存了哪些資訊好了。

查詢當前狀態

1
terraform show

列出所有 resource

1
terraform state list

Change Infrastructure

Change Infrastructure

上半部我們使用 Terraform 建立了一個 Nginx Docker container,接下來我們要使用 Terraform 來修改它。
當使用者修改設定檔 (.tf) Terraform 會建立 「執行清單」(execution plan) 並且 只異動需要變更的部分,藉此滿足 desired state
(與 K8s 撰寫的 YAML 一樣都是在描述 desired state)

Update configuration 更新設定檔

Update configuration

首先先把 main.tf 裡面的 external = 8000 修改成 external = 8080。 接著 terraform apply

terraform_hello_world_5

▲ Docker 無法直接修改 existed container,只能重新建立一個新的。

  • -/+ 代表 Terraform 會 destroy and recreate the resource,而不是原地修改。
  • ~ 代表 Terraform 會原地 (in-place) 修改。

Destroy Infrastructure 刪除 infra

1
terraform destroy

terraform_hello_world_6

▲ 成功刪除!

Define Input Variables 使用變數

Define Input Variables

到目前為止,我們所使用的 value 都是 hard-coded,在這個章節將介紹 Terraform variables。

首先在工作目錄下建立 variables.tf

1
2
3
4
5
variable "container_name" {
  description = "Value of the name for the Docker container"
  type        = string
  default     = "EVG2603Container"
}

接著修改 main.tf 裡面的 docker_container resource block,將 name = "tutorial" 修改成 name = var.container_name

1
2
## apply 
terraform apply

terraform_hello_world_7

▲ 成功建立 “EVG2603Container”

除了可以透過 variables.tf 來定義變數外,在 CLI 也可以 ( 優先權 > variables.tf )。

1
terraform apply -var "container_name=EVG_ACT"

Query Data with Outputs 輸出

Terraform 除了可以 input variables 也可以 output variables。
在工作目錄下建立 outputs.tf

1
2
3
4
5
6
7
8
9
output "container_id" {
  description = "ID of the Docker container"
  value       = docker_container.nginx.id
}

output "image_id" {
  description = "ID of the Docker image"
  value       = docker_image.nginx.id
}

outputs.tf 必須先被 terraform apply

terraform_hello_world_8

▲ “Changes to Outputs”

成功之後我們就可以使用 terraform output 來查詢變數

1
2
3
4
terraform output

container_id = "78a62c03e259bc6b571e1af4ef8c2743549d9e917727e4bc3164877213eba338"
image_id = "sha256:f8f4ffc8092c956ddd6a3a64814f36882798065799b8aedeebedf2855af3395bnginx:latest"

保持帥哥

抱歉,段落標題詐騙 這個段落主要是要讓「機敏資訊」(sensitive data) 不要被直接存在 main.tf 裡面。
如此一來 main.tf 就能夠安心上版控工具了,不然 provider 相關帳號密碼都直接存在 main.tf 裡面上了版控豈不是裸奔嗎?

Protect Sensitive Input Variables

從 Terraform v0.14 後提供 sensitive 這個 variable flag,達成 (√) CLI output 不顯示明文密碼
注意!! 你的密碼還是以 plain text 的形式儲存在 .tfvars 裡面。建議使用 secret management tool 加以處理。

Hands-on

main.tf (沿用上面範例,確保 name = var.container_name 即可)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
terraform {                                                                                                                          
  required_providers {                                                                                                               
    docker = {                                                                                                                       
      source  = "kreuzwerker/docker"                                                                                                 
      version = "~> 2.13.0"                                                                                                          
    }                                                                                                                                
  }                                                                                                                                  
}                                                                                                                                    
                                                                                                                                     
provider "docker" {}                                                                                                                 
                                                                                                                                     
resource "docker_image" "nginx" {                                                                                                    
  name         = "nginx:latest"                                                                                                      
  keep_locally = false                                                                                                               
}                                                                                                                                    
                                                                                                                                     
resource "docker_container" "nginx" {                                                                                                
  image = docker_image.nginx.latest                                                                                                  
  name  = var.container_name                                                                                                         
  ports {                                                                                                                            
    internal = 80                                                                                                                    
    external = 8000                                                                                                                  
  }                                                                                                                                  
}

variables.tf (將 default 移除,新增 sensitive = true )

1
2
3
4
5
variable "container_name" {                                                                                                          
  description = "Value of the name for the Docker container"                                                                         
  type        = string                                                                                                               
  sensitive   = true                                                                                                                 
}

新增 secret.tfvars 絕對要在 .gitignore 裡面!!

1
container_name = "EVG2603Container"

執行 terraform apply -var-file="secret.tfvars" 就不會以明文顯示 container_name 囉!

terraform_hello_world_9

發問

1
2
3
4
5
大家午安,想問一下有關於 Terraform 顯示的部分。
有沒有辦法讓 `terrafrom apply` 時使用 `less` 指令呢?
我用 `terraform apply | less -R` 顏色有了,但沒辦法 page down

用關鍵字: terraform output less 或者 show less 都沒有找到東西

Ans: 使用 tmux scroll buffer ( Ctrl-B + [ + PageUp/Down )