การใช้ count และ for_each ใน Terraform
ถ้าจะใช้ 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 ตัวเข้ามาช่วย ได้แก่
- count
- 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 แค่นั้นเอง