การใช้ count และ for_each ใน Terraform

Nopnithi Khaokaew (Game)
4 min readApr 1, 2022

--

ถ้าจะใช้ Terraform สร้าง resource เดียวกันหลายตัวควรทำยังไง? ความแตกต่างระหว่าง count และ for_each คืออะไร? บทความนี้ผมจะยกตัวอย่างให้เห็นภาพครับ

ถ้าเราจะสร้าง S3 bucket ด้วย Terraform ก็คงจะเขียนประมาณนี้

resource "aws_s3_bucket" "bucket" {
bucket = "nopnithi-bucket"
tags = { Name = "nopnithi-bucket" }
}

แต่ถ้าอยากได้ 3 bucket ก็เขียนแบบนี้สิ

resource "aws_s3_bucket" "bucket1" {
bucket = "nopnithi-bucket1"
tags = { Name = "nopnithi-bucket1" }
}
resource "aws_s3_bucket" "bucket2" {
bucket = "nopnithi-bucket2"
tags = { Name = "nopnithi-bucket2" }
}
resource "aws_s3_bucket" "bucket3" {
bucket = "nopnithi-bucket3"
tags = { Name = "nopnithi-bucket3" }
}

ใช่ แต่ไม่ควรครับ เพราะจะทำให้ code ยาวและ organize ได้ยาก ดังนั้นเราจะใช้ meta-argument ทั้ง 2 ตัวเข้ามาช่วย ได้แก่

  1. count
  2. for_each

ตัวอย่างการใช้ count

ถ้าผมต้องการ S3 ทั้งหมด 3 bucket ก็จะเขียนแบบนี้แทน

resource "aws_s3_bucket" "buckets" {
count = 3
bucket = "nopnithi-bucket${count.index + 1}"
tags = { Name = "nopnithi-bucket${count.index + 1}" }
}
  • ผมเปลี่ยนชื่อ resource เป็น buckets เพื่อจะสื่อว่ามีหลายตัว
  • count.index คือ index ของ element ใน count (เริ่มที่ 0, 1, 2)
  • index ถูกใช้เพื่ออ้างอิงไปหา element หรือ object แต่ละตัวที่เราจะสร้างหรือสร้างขึ้นมาแล้ว
  • นำ count.index ไปใช้ใน bucket name และ +1 เพื่อให้ bucket name เริ่มที่ 1

ผลที่ได้

aws_s3_bucket.buckets[2]: Creating...
aws_s3_bucket.buckets[0]: Creating...
aws_s3_bucket.buckets[1]: Creating...
aws_s3_bucket.buckets[1]: Creation complete after 3s [id=nopnithi-bucket2]
aws_s3_bucket.buckets[0]: Creation complete after 4s [id=nopnithi-bucket1]
aws_s3_bucket.buckets[2]: Creation complete after 4s [id=nopnithi-bucket3]

ปัญหาที่ 1 ของการใช้ count

จากตัวอย่างที่ผ่านมา ถ้าผมอยากลบ bucket ให้เหลือ 2 ผมก็แค่เปลี่ยนเป็น count = 2 ถูกมั้ยครับ? แต่ bucket ตัวไหนจะถูกลบหละ? งั้นผมลองแก้ดูเลยปรากฎว่า bucket 3 (สุดท้าย) จะถูกลบ

# aws_s3_bucket.buckets[2] will be destroyed
# (because index [2] is out of range for count)

อ้าว แต่ถ้าผมอยากลบ bucket 1 จะทำยังไง? คำตอบคือเริ่มจากเปลี่ยนวิธีการสร้างก่อนเลยครับ ดังนั้นผมเขียน code ใหม่แบบนี้

variable "buckets" {
type = list(string)
default = [
"one",
"two",
"three"
]
}
resource "aws_s3_bucket" "buckets" {
count = lenght(var.buckets)
bucket = "nopnithi-bucket-${var.buckets[count.index]}"
tags = { Name = "nopnithi-bucket-${var.buckets[count.index]}" }
}

ผมใช้ฟังก์ชั่น length() นับจำนวน element ในตัวแปร buckets เพื่อนำไปใช้กับ count และแต่ละ element ของ count จะถูกใช้เป็น suffix ของ bucket name แบบนี้

  • nopnithi-bucket-one
  • nopnithi-bucket-two
  • nopnithi-bucket-three

คราวนี้ถ้าผมอยากให้ bucket ตัวไหนหายไปผมก็ลบออกจากตัวแปร buckets เช่น

["one", "two", "three"]

เป็น

["two", "three"]

สุดท้ายผลลัพธ์คือผมจะมี bucket เหลือ 2 ตัว คือ two กับ three

(แต่ปัญหายังไม่หมด) ปัญหาที่ 2 ของการใช้ count

จากด้านบน ถ้าผมต้องการลบ bucket 1 ออก มันก็ได้แหละ แต่สังเกต output ด้านล่างจะเห็นว่า Terraform ลบ bucket ทั้ง 3 ตัวไปจนหมด จากนั้นค่อยสร้างใหม่มาแค่ 2 ตัว แบบนี้ถ้าเป็น resource ที่สำคัญก็งานงอกแน่นอน

Plan: 2 to add, 0 to change, 3 to destroy.
aws_s3_bucket.buckets[1]: Destroying... [id=nopnithi-bucket-two]
aws_s3_bucket.buckets[2]: Destroying... [id=nopnithi-bucket-three]
aws_s3_bucket.buckets[0]: Destroying... [id=nopnithi-bucket-one]
aws_s3_bucket.buckets[0]: Destruction complete after 1s
aws_s3_bucket.buckets[1]: Destruction complete after 1s
aws_s3_bucket.buckets[0]: Creating...
aws_s3_bucket.buckets[1]: Creating...
aws_s3_bucket.buckets[2]: Destruction complete after 1s
aws_s3_bucket.buckets[0]: Creation complete after 2s [id=nopnithi-bucket-two]
aws_s3_bucket.buckets[1]: Creation complete after 3s [id=nopnithi-bucket-three]

สาเหตุเพราะ count มันใช้ index ในการอ้างอิงถึง object ครับ คิดภาพเหมือนเราหยอดลูกบอลลงในภาชนะทรงกระบอก พอเราหยิบ one ออก ทำให้ index ของ resource ที่เหลือเคลื่อนไปด้วย

จากที่ aws_s3_bucket.buckets[0] เป็นการอ้างอิงถึง bucket one ก็กลายเป็น bucket two แทนไปแล้ว ตัวอื่น ๆ ก็เปลี่ยน มันจึงลบบางตัว, ลบทั้งหมด หรืออาจไม่ลบเลย แล้วจึงสร้างใหม่ (ขึ้นกับตำแหน่งของ element ที่ลบออกไปด้วย)

สิ่งที่เราต้องการคือเมื่อเราแก้ตัวแปร buckets เพื่อเอา one ออก มันควรจะลบแค่ bucket one ตัวเดียว และด้วยข้อจำกัดของ count ผมจึงชอบที่จะใช้ for_each แทน

ก่อนไป for_each ผมขอขั้นด้วยตัวอย่างสั้น ๆ ในการใช้ count อีกแบบ

ใช้ count + condition เพื่อตัดสินใจในการสร้าง Resource

variable "enable_s3_bucket" {
type = bool
default = true
}
resource "aws_s3_bucket" "bucket" {
count = var.enable_s3_bucket ? 1 : 0
bucket = "nopnithi-bucket"
tags = { Name = "nopnithi-bucket" }
}

จากตัวอย่างด้านบน ถ้าตัวแปร enable_s3_bucket เป็น true ถึงจะสร้าง S3 bucket ขึ้นมา ถ้าเป็น false ก็จะไม่สร้าง

count = length(var.my_list) > 0 ? 1 : 0

หรือบางทีอาจจะใช้ร่วมกับตัวแปรที่เป็น list เพื่อเช็คว่าถ้ามี element หนึ่งตัวขึ้นไปก็จะสร้าง resource อะไรแบบนี้ก็ได้เหมือนกัน

ตัวอย่างการใช้ for_each

variable "buckets" {
type = list(string)
default = [
"one",
"two",
"three"
]
}
resource "aws_s3_bucket" "buckets" {
for_each = toset(var.buckets)
bucket = "nopnithi-bucket-${each.key}"
tags = { Name = "nopnithi-bucket-${each.value}" }
}

สังเกตว่าตอนนี้ each.key กับ each.value จะได้ค่าเดียวกัน เพราะเราใช้ตัวแปรเป็น list แต่เอาฟังก์ชั่น toset() มาช่วย convert list เป็น set เพื่อให้ใช้กับ for_each ได้

และตอนนี้ Terraform อ้างอิงถึง object โดยใช้ key แทน index แล้ว

aws_s3_bucket.buckets["one"]: Creation complete after 4s [id=nopnithi-bucket-one]

หลังจากนี้ถ้าผมลบ bucket ตัวใดก็ตามออกจากตัวแปร buckets ทาง Terraform ก็จะลบเฉพาะ object หรือ element ตัวนั้น ไม่มีผลกับ bucket อื่น ๆ

ความสามารถที่แท้จริงของ for_each

ที่จริงมันเอามาช่วยให้ loop ในการสร้าง resource ของเราให้ยืดหยุ่นขึ้นด้วย เพราะค่าในตัวแปรที่นำมา iterate นั้นเป็น map (dictionary) ทำให้เราใส่ value ต่าง ๆ ไปใช้ในการสร้าง resource ได้ เช่น

variable "buckets" {
type = map(map(string))
default = {
one = { "environment" : "dev", "enable_object_lock" : false }
two = { "environment" : "uat", "enable_object_lock" : false }
three = { "environment" : "prd", "enable_object_lock" : true }
}
}
resource "aws_s3_bucket" "buckets" {
for_each = var.buckets

bucket = "nopnithi-bucket-${each.key}"
object_lock_enabled = each.value.enable_object_lock
tags = {
Name = "nopnithi-bucket-${each.key}"
Environment = each.value.environment
}
}

จากด้านบนผมใช้ค่าในตัวแปร buckets มาช่วยกำหนดในการสร้าง S3 ด้วย

  • key ใช้เป็น suffix ใน bucket name (one, two และ three)
  • value.environment ใช้เป็น tag:environment(dev, uat และ prd)
  • value.enable_object_lock ใช้กำหนดว่าจะ enable ฟีเจอร์ object lock หรือไม่

ประมาณนี้ครับ ใครที่พอได้ programming อยู่แล้วจะเห็นภาพได้ง่ายมากเลยครับเพราะมันก็คล้ายกับ for loop แหละ ขึ้นกับว่าจะ loop จาก list หรือ dictionary แค่นั้นเอง

--

--

Nopnithi Khaokaew (Game)
Nopnithi Khaokaew (Game)

Written by Nopnithi Khaokaew (Game)

Cloud Solutions Architect & Hobbyist Developer | 6x AWS Certified, CKA, CKAD, 2x HashiCorp Certified (Terraform, Vault), etc.