事半功倍 — 使用 Terragrunt 搭配 Terraform 管理基礎設施

George Chang
Act As A Software Engineer
9 min readMar 21, 2021
Terragrunt

前言

去年年底開始協助團隊導入 Infrastructure as Code (IaC),過往我們是使用基於 Shell script + Cloud Deployment Manager 實作的 deployment script 來佈署產品的基礎設施。

考量到團隊需要同時管理多個產品線,每個產品會用到的基礎設施架構又不盡相同,且團隊開始面臨到需要為特定客戶建立專用環境的情境,因此決定導入擴展性更好的工具來協助團隊快速佈署及管理基礎設施架構。

使用 Terragrunt 搭配 Terraform 管理基礎設施

會選用 Terraform 主要看上用一套工具就能夠支援多雲平台管理的特點,以及靈活與可讀性強的 DSL。

在研究 Terraform 的期間也透過公司的 SRE 大大得知了 Terragrunt 這套工具,Terragrunt 是一套 Terraform 的 wrapper,它補足了一些基於 Terraform 自身限制而做不到的事,也藉此可以讓你的 IaC code 更貼近 DRY 原則

經過評估後,我們決定使用 Terragrunt 搭配 Terraform 來管理所有產品的基礎設施。

我認為搭配使用 Terragrunt 可以帶來的重要效益:

  1. 有效率地管理 remote state 設定
  2. 將 backend Storage Bucket 納入管理
  3. 管理 module 之間的相依性
  4. 保持良好的開發原則

有效率地管理 remote state 設定

Terraform 在執行佈署後會產生 state file,藉此紀錄追蹤你的基礎設施狀態。在共同協作時就需要設定 remote state backend,讓團隊成員都可以存取 Terraform state files。

當我已經熟悉 Terraform 的管理方式,我便面臨到第一個問題:

「如何根據不同環境設定不同的 remote state backend?」

例如我想將 production 環境的 remote state 存放在 production project 的 GCS bucket 內,staging 環境的 remote state 存放在 staging project 內,以此類推。

但是目前 Terraform 的 backend config 不支援使用變數做為參數,也就是想做到上述需求就得根據不同環境寫出不同的 backend config 再傳入。

Terragrunt 做為 Terraform 的 wrapper,透過定義 remote_state 的 generate block ,就可以在執行前自動產生 backend.tf,解決了這個問題。

remote_state {
backend = "gcs"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
...
}

將 backend Storage Bucket 納入管理

我們打算使用 GCS 做為 Terraform 的 remote state backend,但是在設定 GCS backend 時需要填入預先建立好的 GCS bucket name。

此時就面臨到「先有雞還是先有蛋」的問題:

我想要用 Terraform 來管理所有的基礎設施,避免「手動」佈署,但是在執行 Terraform 之前還需要先「手動」建立 GCS bucket。

如果使用 Terragrunt 就可以自動建立 GCS bucket,簡單解決了 remote state backend 得脫離 IaC Project 管理的問題,或者避免得用一些奇技淫巧來解這個問題。

下面是設定 GCS backend 的範例:

# ./environments/terragrunt.hclremote_state {
backend = "gcs"

config = {
project = "my-gcp-project"
location = "asia"
bucket = "my-gcp-project-tfstate"
prefix = "${path_relative_to_include()}"
}
}

做了設定後執行 Terragrunt command,Terragrunt 就會自動檢查 GCS bucket 是否存在,如果不存在就可以自動幫你建立。

管理 module 之間的相依性

Terragrunt 提供 dependency 與 dependencies block 讓我們可以方便地管理基礎設施之間的相依性。

舉個簡單的例子,當我們今天需要建立一座 TCP Load Balancer,但建立之前需要先保留 IP address,此時就可以依靠 dependency block 來完成需求。

# ./environments/staging/terraform-google-lb/terragrunt.hcldependency "address_lb" {
config_path = "../address-lb"
}

inputs = {
ip_address = dependency.address_lb.outputs.address
}

如此 Terragrunt 便會自動幫你管理基礎設施佈署的相依性,並在建立 TCP Load Balancer 時取得相依的 IP address 完成佈署。

接著你也可以透過 Terragrunt command 檢視 dependencies:

terragrunt graph-dependencies | dot -Tsvg > graph.svg

保持良好的開發原則

一個 Terragrunt config 必須定義 source 與 inputs,定義的 source URL 必須是一個 Terraform module,而在 inputs block 定義要傳入 module 的參數設定,這個規範有幾個好處:

  • 保持良好的開發原則

藉由 source URL 必須是一個 Terraform module 的特性,間接鼓勵保持開發者實作可重用的 module 的好習慣。

  • 減少了多環境的設定成本

這也是藉由上述特性,所有的 resource 設定都放在 module 內,在 Terragrunt config 內只包含了 source URL & input variables,當我們需要做多個環境的設定時,大多只要先做好一個環境,再將 Terragrunt configs 複製到其他環境後便可完成。

  • 可直接使用公開的 Terraform module

例如 Google 就在 Github 上發布了許多常用的 modules:Terraform modules for Google Cloud 可以直接使用,省下開發維護 Terraform code 的成本。

要說缺點也不是沒有,強迫實作 Terraform module 增加了開發與維護的時間成本。試想比起聲明一個特定的 resource,當你要實作一個可重複使用的 Terraform module,需要考慮的 input/output 以及 resource 的參數處理上勢必會複雜許多。

優點的反面即是缺點,在 backend 的世界,一切都是 trade-off。

考量到就算我們單純只使用 Terraform,最後也一定會走向盡可能實作 Terraform module 來避免重複定義 resources,更可以針對 module 做 unit testing,因此這一點對團隊來說還是利大於弊的。

補充:那麼 Terraform 內建的 workspace 如何?

一開始研究 Terraform 功能時,就發現 Terraform 有提供 workspace 功能。

簡單地說,每個 workspace 都有獨立的 state file,藉此可以建立多個 workspace 來管理對應的環境,例如:

terraform workspace new staging
terraform workspace new qa
terraform workspace new production
terraform workspace select staging

不過評估之後,我認為使用 workspace 有下列缺點:

  • 難以應付「不同的環境的基礎設施設定不同」的情境

雖然理想上會希望每個環境的基礎設施設定相同,不過現實可能會因為成本考量使得各個環境有所差異。

但是多個 workspace 會共用同一份 Terraform configs,若是有些基礎設施在測試環境不需要佈署,變成還得直接在 config hardcode 來指定。

resource "google_compute_instance" "test-instance" {
name = "test-instance"
count = "${terraform.workspace == "production" ? 1: 0}"

又或者要建立 map variable 來處理:

locals {
test_instance_count = {
staging = 0
production = 0
}
}
resource "google_compute_instance" "test-instance" {
name = "test-instance"
count = test_instance_count[terraform.workspace]

這種設定一多其實還滿難維護的。

  • 難以直接知道我現在處於哪個 workspace

在執行 Terraform command 之前,我一定得透過 terraform workspace list 或者 terraform workspace show 來得知當前選擇的 workspace,當然你也可以安裝 zsh plugin 來解決這個問題。

不過我覺得這一點還是相當有危險性,很容易因為誤操作就修改了其他環境甚至是 production 環境的基礎設施。

考量這些因素,最後還是決定選用 Terragrunt 來處理。

如果你需要管理的環境單純,那麼我非常建議直接使用 Terraform 內建的 workspace 來處理,就不用得多用一套工具增加複雜度。

--

--