Day 07-Terraform 写起来不够 DRY 的问题,这解 Terragrunt 你试试看

课程内容与代码会放在 Github 上: https://github.com/chechiachang/terraform-30-days

赛後文章会整理放到个人的部落格上 http://chechia.net/

追踪粉专可以收到文章的主动推播

https://ithelp.ithome.com.tw/upload/images/20210901/20120327NvpHVr2QC0.jpg


Reviews

前六堂课程,我们简单认识 Terraform 的核心观念,包括 State & Backend,以及Module 使用,已经可以在工作上实际使用 Terraform。然而,随着使用时间越长,使用的 module 越多,开始会发现许多 Terraform 的程序码会不断的重复,违反 DRY 原则,例如:

  • provider.tf 每个 root module 都存在,而且内容几乎一样
    • 其中 backend 都是使用 azurerm,使用 storage container,设定只有 key 不同
    • 还记得 _poc/container_registry/ 这边的范例,一堆 soft link provider.tf 吗
  • variables.tf 有许多重复的参数
    • 许多 module 都需要传入 resource group 参数,而本篇所有的 example 都使用相同的 resource group
    • location 参数在相同 location 路径下都是一样的,ex. 都是"southeastasia"

Ex. Registry 的 input arguments 中许多参数,跟别的 root module 重复

# _poc/container_registry/provider.tf

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 2.65.0"
    }
  }

  required_version = ">= 1.0.1"

  # remote Backend
  backend "azurerm" {
    resource_group_name  = "terraform-30-days"
    storage_account_name = "tfstatee903f2ef317fb0b4"
    container_name       = "tfstate"
    key                  = "container_registry.tfstate" # 唯一不一样的设定
  }

Ex. Registry 的 input arguments 中许多参数,跟别的 root module 重复

# _poc/container_registry/registry.tf

location = "southeastasia"
resource_group_name  = "terraform-30-days"

Don't Repeat Youself 是一个软件工程开发的基本原则,不断重复的程序码往往代表未来维护的困难,违反 DRY 不一定代表不好,在某些情形工程师可能会选择更好的可读性,而牺牲 DRY。Terraform Lanuguage 由於语言特性,有一部份重复的程序码。

这部分重复的程序码,会随团队使用 Terraform 的规模线性成长,对於管理大量 Terraform 的维护人员造成困扰,也拖慢开发进度。

因此,在开始接触大量复杂范例之前,我们选择先导入 Terragrunt 这个工具,来精简程序码。

Terragrunt

Terragrunt 是 gruntwork 推出的一个 Terraform thin wrapper,在执行 Terraform 前可以先"调整" root module 内的 .tf 档案,保持程序码的精简,并提供许多而外工具,加速开发

这里附上 Gruntwork 官方的 Get Started Guide

Install Terragrunt

可以直接到 Github release 页面 下载 binary

wget https://github.com/gruntwork-io/terragrunt/releases/download/v0.31.3/terragrunt_darwin_amd64

chmod +x terragrunt_darwin_amd64

sudo mv terragrunt_darwin_amd64 /usr/local/bin/terragrunt

如果使用 pkg manager 或其他工具,也可以直接使用

brew install terragrunt

安装完可以使用 terragrunt

terragrunt --help

VERSION:
   v0.31.3

Config Terragrunt

接下来我们要设定 terragrunt。由於Terragrunt 有非常多的功能,这边我们先专注在两个需求:

  • provider / variable 程序码精简
  • Backend / State 设定是否可以自动化

这边我们直接看范例,首先要说明整体资料夹结构:

tree -L 1 azure

azure
├── _poc
├── foundation
├── dev
│   └── southeastasia
├── modules
├── prod
├── stag
├── test
├── terragrunt.hcl
└── env.tfvars
  • azure/_poc 是使用 terragrunt 之前我们所有的范例
  • 由於之後会介绍多环境的管理,所有的 .tf 都可以在不同环境,产生一模一样的 resources,这边先开启四个环境,可以参考 terrgrunt 文件:多环境的支援
    • foundation
    • dev
    • stag
    • prod
  • modules 是本地维护的 local module
  • test 是 .tf 的测试档案,我们之後会讲解如何为 .tf 撰写测试
  • dev/southeastasia 存放 dev 环境的 southeastasia
    • 许多 resource 放置在公有云不同的 location
    • 实务也常见,在不同 location 产生相同 resource 做跨区 replicas 以支撑可用性
    • 接下来范例主要会使用 azure/dev/southeastasia

那剩下两个东西是什麽?

  • terragrunt.hcl
  • env.tfvars

这是与 terragrunt 设定有关,请见底下的说明。

Example: Terraform Backend

首先我们可以在产生一组 terraform backend

  • resource group: terraform-30-days
  • 之前使用的可以选择整个删除掉 (resource group: terraform-30-days-poc)
  • 这样可以确保 resource gropu 内的资源都是 terragrunt 产生的,会比较乾净
  • 选择要沿用 terraform-30-days-poc resource gropu 也不是不行

azure/foundation/southeastasia/terraform_backend,一看,里面只剩下一个档案

# terragrunt.hcl
# TERRAGRUNT CONFIGURATION

terraform {
  source = "../../../..//azure/modules/terraform_backend"
}

# dependency cycle: terraform_backend is provisioned before all terragrunt usage. There is no terragrunt.hcl at that time.
#include {
#  path = "${find_in_parent_folders()}"
#}

inputs = {
  resource_group_name = "terraform-30-days"
  location            = "southeastasia" # Or use japaneast
}
  • source 大家应该很熟悉了,告诉 terragrunt 我们的 root module 路径
    • Review: 在使用 terraform 而非 terragrunt 时,我们可以指定 local module path 来使用
      odule,这边原理类似
    • 要注意这边的是 terragrunt 的 source 参数,虽然原理相同,但不等於 terraform 的 source
      • 要分清楚 .hcl 内部的是 terragrunt 的 config
      • 要分清楚 .tf 内部的是 terraform 的 config
  • 由於使用 local module,这边也透过 inputs 传入

接下来进行 init 与 plan。我们把新的 terraform backend 使用 terragrunt 产生出来

terragrunt init
terragrunt plan
terragrunt apply

到这边,使用起来跟直接使用 terraform 应该没有差异,上面这个例子并没有使用 terragrunt 的额外功能。terragrunt 单纯把 terraform 的 command 传递下去,底下还是执行 terraform。

terragrunt 的功能,下个例子就会展现。

Example: compute network

网路 / VPC 网段的管理,是公有云的必要工作,这边以 provision 新的网段为例

azure/foundation/compute_network,里面有两个档案

tree
.
├── compute_network.tf
└── terragrunt.hcl

provider.tf 在这边已经消失了

看一下 terragrunt.hcl 的设定,重点在 include {} 这个 code block

  • find_in_parent_folders()terragrunt 提供的 function
    • 会一路像上层资料夹,搜寻 terragrunt.hcl,并回传绝对路径
    • 这个例子就会变成:~/terraform-30-days/azure/terragrunt.hcl
  • include code block 是 terragrunt 继承其他的 terragrunt.hcl 设定
    • 我们希望重复使用 provider.tf 的设定,所以把他放到上层 terragrunt.hcl 内部
    • 然後再用 include ,在 terragrunt command 时 (terraform command 前)动态载入
# terragrunt.hcl
# TERRAGRUNT CONFIGURATION

# use terragrunt function to include .hcl file
# in this case, will find azure/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

terraform {
  source  = "../../..//azure/foundation/compute_network"
  # use double-slash (//) after repository root path to avoid
  # - WARN[0000] No double-slash (//) found in source URL
  #source  = "."
}

底下的 terraform {} code block 则跟上一个例子一样,指向 root module

  • 仔细一看,../../../..//azure/modules/terraform_backend 其实就是 . 也就是当前所在路径
  • 之所以使用较长的路径,是为了帮助 terraform 找到 git repository 的 root path
  • Review: Git remote module
    • 使用 local module,相对路径不会影响 terraform 找寻 local module
    • 如果使用 git remote module,有没有 double slash 就会影响 terraform 能否顺利找到 module 路径

接下来看上层 include 的 ~/terraform-30-days/azure/terragrunt.hcl 内容

首先是 provider.tf,这边使用 generate {} code block 来产生 provider.tf

  • 会在 source 的目录(也就是执行 terraform 的目录)下产生 provider.tf
  • 如果要调整 provider.tf 的参数,这里也支援使用 terragrunt 的 function 与变数,这边先不使用
# azure/terragrunt.hcl

generate "provider" {
  path = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
provider "azurerm" {
  features {}
}
EOF
}

再来产生的事 backend.tf,使用 remote_state {} code block 设定 remote backend

  • Review: 我们使用 azurerm + storage container
  • 这边使用 generate,原理与 generate block 相同,在 terraform 的 root module 内产生 backend.tf
  • 在 backend.tf 内设定 storage container 的参数
# azure/terragrunt.hcl

remote_state {
  backend = "azurerm"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    key = "${path_relative_to_include()}/terraform.tfstate"
    resource_group_name  = "terraform-30-days"
    storage_account_name = "tfstate445d2966b56b5d05"
    container_name       = "tfstate"
  }
}

path_relative_to_include() 是另一个 terragrunt function

  • 与 include {} 搭配使用,回传"从目前的 terragrunt.hcl 到 include{} terragrunt.hcl 路径的相对路径"
  • 目前 azure/foundation/compute_network/terragrunt.hcl
  • include azure/terragrunt.hcl
  • 这个范例 path_relative_to_include() = foundation/compute_network
├── terragrunt.hcl
├── env.tfvars
├── foundation
│   ├── compute_network
│   │   ├── compute_network.tf
│   │   └── terragrunt.hcl

所以整个效果等同於产生一个 backend.tf

# backend.tf

terraform {
  backend "azurerm" {
    key = "foundation/compute_network/terraform.tfstate"
    resource_group_name  = "terraform-30-days"
    storage_account_name = "tfstate445d2966b56b5d05"
    container_name       = "tfstate"
  }
}

为何 key 要设为 foundation/compute_network/terraform.tfstate

  • 我们希望 terraform.tfstate 放到 azure storage blob 中,也能按照一定的逻辑存放,方便管理
  • 所以使用 terragrunt.hcl 彼此的相对位置
  • foundation/compute_network/terragrunt.hcl
  • 产生的 state 就在 blob//foundation/compute_network/terraform.tfstate

最後一段 terraform {} block,可以在 terragrunt 驱动的 terraform command 做许多调整,例如

  • 这边增加 extra_arguments
    • get_terraform_commands_that_need_vars(),回传一串接受 -var 与 -var-file 参数的 terraform command
    • required_var_files 参数把 env.tfvars 档案,作为 terraform -var-file 的参数
    • 搭配 get_parent_terragrunt_dir() 使用,拿到上层 terragrunt.hcl 的绝对路径
    • 然後读取这个档案 ~/terraform-30-days/azure/env.tfvars,作为 -var-file 的参数
# azure/terragrunt.hcl

terraform {
  extra_arguments "env" {
    commands = get_terraform_commands_that_need_vars()
    required_var_files = [
      "${get_parent_terragrunt_dir()}/env.tfvars",
    ]
  }
}

效果等同在 terraform plan 与 apply (以及其他 command) 执行时,多加参数

  • env.tfvars 里面的 variable 就会是所有 root module 中执行 terraform command 时都吃得到
  • 只需要维护一组 env.tfvars
terraform plan -var-file ~/terraform-30-days/azure/env.tfvars
terraform apply -var-file ~/terraform-30-days/azure/env.tfvars

Cache

terragrunt 会将 terraform module cache 一分在本目录,cache

如果是在本地开发中的 module,有可能会 cache 到错误的 module,请把本地 cache 清除再重新 init

rm -rf .terragrunt-cache

terragrunt init

Summary

terraform 的命令在 terragrunt 上完全都能使用,所以才说 terragrunt 是一层 wrapper,意思是

  • terragrunt 只是在执行 terraform command 前,先对 .tf 档案动一些手脚
  • 对 terraform command 动一些手脚

Pros & Cons

使用 Terragrunt 是一个额外的选择,团队可以依据状况去选择

Pros

  • 精简程序码,包含 provider / backend ...
  • runtime 注入变数
  • 在 terraform 的 lifecycle 之前,与之後执行额外的程序

Cons

  • 程序码会变得更复杂
  • 需要额外注意 terragrunt 与 terraform 之间的 lifecycle
  • 不熟悉时 debug 可能造成一些麻烦

<<:  Innodb资料页结构-Part2(页面目录、页面表头、档案表头、档案结尾)

>>:  D7 - 如何用 Google Apps Script 将 Google 表单的回应即时同步在多个行事历上?

[iT铁人赛Day4]JAVA的运算符号

今天来讲JAVA的运算符号 JAVA的运算符号有分很多种,例如: " + "加 ...

Day 1 : 前言+本系列会使用到的东西(vscode、xampp、virtualbox、ubuntu、python安装说明)

前言: 大家好,这是我第一次参加铁人赛 主要是想记录一下自己学过的东西 并和大家分享一些我觉得很重要...

Day24 测试写起乃 - Guard

Guard 可以帮助你在开发时监听,并启用测试,让你在开发阶段可以顺便跑测试,当然我也是第一次玩XD...

Day21. Blue Prism 有事不再找老大,自己搞定 -BP 的Exception Type 与Exception Handling

常听说出事了就想找老大摆平, 结果在群内的地位越来越低, 如果能试着自己处理问题, 把事情Handl...

Day-4 CLA以及bit乘法

CLA以及bit乘法 tags: IT铁人 例题答案 就简单把答案打出来罗~ 小心转换成二位元不要粗...