Terraform If/Else Koşullar (Conditionals)
Terraform If/Else koşul ifadeleri ile iki değerden birini seçmek için bir boolean ifadenin değerini kullanabilirsiniz.
Terraform döngüleri kullanmak için birkaç farklı yol sunması gibi, her biri biraz farklı bir senaryoda kullanılması amaçlanan koşulların kullanılması için de birkaç farklı yolu vardır:
count
ifadesi: Koşullu kaynaklar için kullanılırfor_each
vefor
ifadeleri: Bir kaynak içindeki koşullu kaynaklar ve satır içi bloklar için kullanılırif
string directive: Bir string içindeki koşullu ifadeler için kullanılır
Bunların her birinin üzerinden teker teker geçelim.
1. count
İfadesiyle Koşullar
Daha önce gördüğünüz count
parametresi, temel bir döngü yapmanızı sağlar. Aynı mekanizmayı temel bir koşul yapmak için kullanabilirsiniz. Bir sonraki bölümdeki if
ifadelerine bakarak başlayalım ve sonraki bölümde if-else
ifadelerine geçelim.
1.1. If
İfadesi ile count
Kullanımı
Daha önceki yazılarımızda web server cluster dağıtmak için bir "blueprint" olarak kullanılabilecek bir Terraform modülü oluşturdunuz. Modül, bir Otomatik Ölçeklendirme Grubu (ASG), Uygulama Yük Dengeleyici (ALB), güvenlik grupları ve bir dizi başka kaynak oluşturmuştu. Modülün oluşturmadığı bir şey zamanlanmış eylemdi. Kümeyi yalnızca productionda ölçeklendirmek istediğiniz için, aws_autoscaling_schedule
kaynaklarını doğrudan live/prod/services/webserver-cluster/main.tf
altındaki production yapılandırmalarında tanımladınız. Web server cluster modülündeki aws_autoscaling_schedule
kaynaklarını tanımlamanın ve bunları modülün bazı kullanıcıları için koşullu olarak oluşturmanın ve diğerleri için oluşturmamanın bir yolu var mıdır?
İlk adım, modülün otomatik ölçeklendirmeyi etkinleştirip etkinleştirmeyeceğini belirtmek için kullanabileceğiniz modules/services/webserver-cluster/variables.tf
dosyasına bir Boolen giriş değişkeni eklemektir:
variable "enable_autoscaling" {
description = "If set to true, enable auto scaling"
type = bool
}
Şimdi, genel amaçlı bir programlama dili kullanıyor olsaydınız (Python gibi), bu giriş değişkenini bir if
ifadesinde kullanabilirdiniz:
# Bu sadece bir pseudo koddur. Terraform ile çalışmayacaktır.
if var.enable_autoscaling {
resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
scheduled_action_name = "${var.cluster_name}-scale-out-during-business-hours"
min_size = 2
max_size = 10
desired_capacity = 10
recurrence = "0 9 * * *"
autoscaling_group_name = aws_autoscaling_group.example.name
}
resource "aws_autoscaling_schedule" "scale_in_at_night" {
scheduled_action_name = "${var.cluster_name}-scale-in-at-night"
min_size = 2
max_size = 10
desired_capacity = 2
recurrence = "0 17 * * *"
autoscaling_group_name = aws_autoscaling_group.example.name
}
}
Terraform if
ifadelerini desteklemez, bu nedenle bu kod çalışmayacaktır. Ancak, aynı şeyi count
parametresini kullanarak ve iki özellikten yararlanarak gerçekleştirebilirsiniz:
- Bir kaynakta
count
parametresini 1 olarak ayarlarsanız, o kaynağın bir kopyasını alırsınız;count
parametresini 0 olarak ayarlarsanız, bu kaynak hiç oluşturulmaz. - Terraform,
<CONDITION> ? <TRUE_VAL> : <FALSE_VAL>
formatındaki koşul ifadelerini destekler . Diğer programlama dillerinden aşina olabileceğiniz bu üçlü sözdizimiCONDITION
içindeki Boolean mantığını değerlendirecek ve eğer sonuç true iseTRUE_VAL
, sonuç false iseFALSE_VAL
döndürecektir.
Bu iki fikri bir araya getirerek web server cluster modülünü aşağıdaki gibi güncelleyebilirsiniz:
resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
count = var.enable_autoscaling ? 1 : 0
scheduled_action_name = "${var.cluster_name}-scale-out-during-business-hours"
min_size = 2
max_size = 10
desired_capacity = 10
recurrence = "0 9 * * *"
autoscaling_group_name = aws_autoscaling_group.example.name
}
resource "aws_autoscaling_schedule" "scale_in_at_night" {
count = var.enable_autoscaling ? 1 : 0
scheduled_action_name = "${var.cluster_name}-scale-in-at-night"
min_size = 2
max_size = 10
desired_capacity = 2
recurrence = "0 17 * * *"
autoscaling_group_name = aws_autoscaling_group.example.name
}
var.enable_autoscaling
true
ise, aws_autoscaling_schedule
kaynaklarının her biri için count
parametresi 1'e ayarlanır, böylece her birinden bir adet oluşturulur. var.enable_autoscaling
false
ise, aws_autoscaling_schedule
kaynaklarının her biri için count
parametresi 0'a ayarlanır, dolayısıyla hiçbiri oluşturulmaz. Bu tam olarak istediğiniz koşullu mantık!
enable_autoscaling
öğesini false
olarak ayarlayarak otomatik ölçeklendirmeyi devre dışı bırakmak için artık bu modülün staging ortamındaki kullanımını güncelleyebilirsiniz (live/stage/services/webserver-cluster/main.tf
'de):
module "webserver_cluster" {
source = "../../../../modules/services/webserver-cluster"
cluster_name = "webservers-stage"
db_remote_state_bucket = "(YOUR_BUCKET_NAME)"
db_remote_state_key = "stage/data-stores/mysql/terraform.tfstate"
instance_type = "t2.micro"
min_size = 2
max_size = 2
enable_autoscaling = false
}
Benzer şekilde, enable_autoscaling
'i true
olarak ayarlayarak otomatik ölçeklendirmeyi etkinleştirmek için production ortamda (live/prod/services/webserver-cluster/main.tf
'de) bu modülün kullanımını güncelleyebilirsiniz (ayrıca, özel aws_autoscaling_schedule
kaynaklarını da kaldırdığınızdan emin olun):
module "webserver_cluster" {
source = "../../../../modules/services/webserver-cluster"
cluster_name = "webservers-prod"
db_remote_state_bucket = "(YOUR_BUCKET_NAME)"
db_remote_state_key = "prod/data-stores/mysql/terraform.tfstate"
instance_type = "m4.large"
min_size = 2
max_size = 10
enable_autoscaling = true
custom_tags = {
Owner = "team-foo"
ManagedBy = "terraform"
}
}
Bu yaklaşım, kullanıcı modülünüze açık bir Boolen değeri iletirse işe yarar, ancak Boolean, string eşitliği gibi daha karmaşık bir karşılaştırmanın sonucuysa ne yaparsınız? Daha karmaşık bir örnek üzerinden gidelim.
Web sunucusu küme modülünün bir parçası olarak bir dizi CloudWatch alarmı oluşturmak istediğinizi hayal edin. Belirli bir ölçüm önceden tanımlanmış bir eşiği aşarsa, çeşitli mekanizmalar (ör. e-posta, metin mesajı) aracılığıyla sizi bilgilendirmek için bir CloudWatch alarmı yapılandırabilirsiniz. Örneğin, 5 dakikalık bir süre içinde kümedeki ortalama CPU kullanımı %90'ın üzerindeyse çalan bir alarm oluşturmak için modules/services/webserver-cluster/main.tf
'deki aws_cloudwatch_metric_alarm
kaynağını nasıl kullanabileceğiniz aşağıda açıklanmıştır:
resource "aws_cloudwatch_metric_alarm" "high_cpu_utilization" {
alarm_name = "${var.cluster_name}-high-cpu-utilization"
namespace = "AWS/EC2"
metric_name = "CPUUtilization"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.example.name
}
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
period = 300
statistic = "Average"
threshold = 90
unit = "Percent"
}
Bu, bir CPU Utilization alarmı için iyi sonuç verir, ancak CPU credit düşük olduğunda çalan başka bir alarm eklemek isterseniz ne olur? Web sunucusu kümenizin CPU crediti bitmek üzereyken çalan bir CloudWatch alarmı aşağıda verilmiştir:
resource "aws_cloudwatch_metric_alarm" "low_cpu_credit_balance" {
alarm_name = "${var.cluster_name}-low-cpu-credit-balance"
namespace = "AWS/EC2"
metric_name = "CPUCreditBalance"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.example.name
}
comparison_operator = "LessThanThreshold"
evaluation_periods = 1
period = 300
statistic = "Minimum"
threshold = 10
unit = "Count"
}
Buradaki sorun, CPU creditlerin yalnızca tXXX instancelar (ör. t2.micro
, t2.medium
vb.) için geçerli olmasıdır. Daha büyük instance türleri (ör. m4.large
) CPU credit kullanmaz ve bir CPUCreditBalance
metriği raporlamaz. Bu nedenle, bu instancelar için böyle bir alarm oluşturursanız, alarm her zaman INSUFFICIENT_DATA
durumunda kalır. Öyleyse yalnızca var.instance_type
“t” harfiyle başlıyorsa alarm oluşturmanın bir yolu var mıdır?
var.is_t2_instance
adında yeni bir Boolen giriş değişkeni ekleyebilirsiniz, ancak bu var.instance_type
ile gereksiz olacaktır ve büyük olasılıkla birini güncellerken diğerini güncellemeyi unutursunuz. Daha iyi bir alternatif, koşul kullanmaktır:
resource "aws_cloudwatch_metric_alarm" "low_cpu_credit_balance" {
count = format("%.1s", var.instance_type) == "t" ? 1 : 0
alarm_name = "${var.cluster_name}-low-cpu-credit-balance"
namespace = "AWS/EC2"
metric_name = "CPUCreditBalance"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.example.name
}
comparison_operator = "LessThanThreshold"
evaluation_periods = 1
period = 300
statistic = "Minimum"
threshold = 10
unit = "Count"
}
Alarm kodu, nispeten karmaşık count
parametresi dışında öncekiyle aynıdır:
count = format("%.1s", var.instance_type) == "t" ? 1 : 0
Bu kod, var.instance_type
öğesinden yalnızca ilk karakteri almak için format
işlevini kullanır. Bu karakter bir "t
" ise (örneğin, t2.micro
), sayımı 1'e ayarlar; aksi takdirde, sayıyı 0'a ayarlar. Bu şekilde, alarm yalnızca gerçekten bir CPUCreditBalance
metriği olan instance türleri için oluşturulur.
1.2. If-Else
İfadesi ile count
Kullanımı
Artık if
ifadesini nasıl yapacağınızı öğrendiğinize göre, if-else
ifadesine bakabiliriz.
Önceki yazımızda, EC2'ye salt okunur erişimi olan birkaç IAM kullanıcısı oluşturdunuz. Bu kullanıcılardan biri olan ismet'e CloudWatch erişimi de vermek istediğinizi, ancak Terraform yapılandırmalarını uygulayan kişinin ismet'e yalnızca okuma erişimi mi yoksa hem okuma hem de yazma erişimi mi atanacağına karar vermesine izin vermek istediğinizi hayal edin. Bu biraz yapmacık bir örnek, ancak önemli olan tek şeyin if veya else dallarından birinin yürütüldüğü ve Terraform kodunun geri kalanının yürütülmediği basit bir if-else ifadesi türünü göstermeyi kolaylaştırıyor.
CloudWatch'a salt okunur erişime izin veren bir IAM politikası:
resource "aws_iam_policy" "cloudwatch_read_only" {
name = "cloudwatch-read-only"
policy = data.aws_iam_policy_document.cloudwatch_read_only.json
}
data "aws_iam_policy_document" "cloudwatch_read_only" {
statement {
effect = "Allow"
actions = [
"cloudwatch:Describe*",
"cloudwatch:Get*",
"cloudwatch:List*"
]
resources = ["*"]
}
}
Ve işte CloudWatch'a tam (okuma ve yazma) erişime izin veren bir IAM politikası:
resource "aws_iam_policy" "cloudwatch_full_access" {
name = "cloudwatch-full-access"
policy = data.aws_iam_policy_document.cloudwatch_full_access.json
}
data "aws_iam_policy_document" "cloudwatch_full_access" {
statement {
effect = "Allow"
actions = ["cloudwatch:*"]
resources = ["*"]
}
}
Amaç, give_ismet_cloudwatch_full_access
adlı yeni bir girdi değişkeninin değerine dayalı olarak bu IAM politikalarından birini ismet'i eklemektir:
variable "give_ismet_cloudwatch_full_access" {
description = "If true, ismet gets full access to CloudWatch"
type = bool
}
Genel amaçlı bir programlama dili kullanıyor olsaydınız (Python gibi), şuna benzeyen bir if-else ifadesi yazabilirsiniz:
# Bu sadece pseudo koddur. Terraform ile bu kod çalışmayacaktır.
if var.give_neo_cloudwatch_full_access {
resource "aws_iam_user_policy_attachment" "neo_cloudwatch_full_access" {
user = aws_iam_user.example[0].name
policy_arn = aws_iam_policy.cloudwatch_full_access.arn
}
} else {
resource "aws_iam_user_policy_attachment" "neo_cloudwatch_read_only" {
user = aws_iam_user.example[0].name
policy_arn = aws_iam_policy.cloudwatch_read_only.arn
}
}
Bunu Terraform'da yapmak için, kaynakların her birinde count
parametresini ve bir koşul ifadesini kullanabilirsiniz:
resource "aws_iam_user_policy_attachment" "ismet_cloudwatch_full_access" {
count = var.give_ismet_cloudwatch_full_access ? 1 : 0
user = aws_iam_user.example[0].name
policy_arn = aws_iam_policy.cloudwatch_full_access.arn
}
resource "aws_iam_user_policy_attachment" "ismet_cloudwatch_read_only" {
count = var.give_ismet_cloudwatch_full_access ? 0 : 1
user = aws_iam_user.example[0].name
policy_arn = aws_iam_policy.cloudwatch_read_only.arn
}
Bu kod, iki aws_iam_user_policy_attachment
kaynağı içerir. CloudWatch tam erişim izinlerini ekleyen ilki, var.give_ismet_cloudwatch_full_access
doğruysa 1, aksi takdirde 0 olarak değerlendirilecek (bu if cümlesidir) koşullu bir ifadeye sahiptir. CloudWatch salt okunur izinlerini ekleyen ikincisi, tam tersini yapan ve var.give_ismet_cloudwatch_full_access
doğruysa 0, aksi takdirde 1 olarak değerlendiren bir koşullu ifadeye sahiptir.
Artık bir if/else koşuluna dayalı olarak bir kaynak veya diğerini oluşturma yeteneğine sahip olduğunuza göre, gerçekten oluşturulan kaynakta bir özniteliğe erişmeniz gerekirse ne yaparsınız? Örneğin, gerçekte eklediğiniz ilkenin ARN'sini içeren ismet_cloudwatch_policy_arn
adlı bir çıktı değişkeni eklemek isterseniz ne olur?
output "neo_cloudwatch_policy_arn" {
value = (
var.give_ismet_cloudwatch_full_access
? aws_iam_user_policy_attachment.ismet_cloudwatch_full_access[0].policy_arn
: aws_iam_user_policy_attachment.ismet_cloudwatch_read_only[0].policy_arn
)
}
Bu şimdilik işe yarayacaktır, ancak bu kod biraz kırılgan oldu: aws_iam_user_policy_attachment
kaynaklarının count
parametresindeki koşulu değiştirirseniz, belki gelecekte bu yalnızca var.give_ismet_cloudwatch_full_access
'e değil, birden çok değişkene bağlı olacaktı. Bu sefer çıktı değişkeninde koşul değişkenini güncellemeyi unutma riskiniz vardır ve sonuç olarak, var olmayabilecek bir string öğesine erişmeye çalışırken çok kafa karıştırıcı bir hata alırsınız.
Daha güvenli bir yaklaşım, concat
ve one
işlevlerinden yararlanmaktır. concat
işlevi girdi olarak iki veya daha fazla liste alır ve bunları tek bir listede birleştirir. one
işlevi bir listeyi girdi olarak alır ve listede hiç öğe yoksa, null
döndürür; listede 1 eleman varsa, o elemanı döndürür; ve listede 1'den fazla eleman varsa, bir hata gösterir. Bu ikisini bir araya getirip bir uyarı ifadesi ile birleştirerek şunları elde edersiniz:
output "neo_cloudwatch_policy_arn" {
value = one(concat(
aws_iam_user_policy_attachment.ismet_cloudwatch_full_access[*].policy_arn,
aws_iam_user_policy_attachment.ismet_cloudwatch_read_only[*].policy_arn
))
}
if/else koşulunun sonucuna bağlı olarak, ismet_cloudwatch_full_access
boş olacak ve ismet_cloudwatch_read_only
bir öğe içerecek veya tam tersi olacak, bu nedenle bunları bir araya getirdiğinizde, tek öğeli bir listeniz olacak ve işlev o tek elemanı geri döndürecek. Bu, if/else koşulunuzu nasıl değiştirirseniz değiştirin düzgün çalışmaya devam edecektir.
If-else ifadelerini simüle etmek için count
ve built-in işlevleri kullanmak biraz zor olsa da oldukça iyi çalışan bir yöntemdir ve koddan da görebileceğiniz gibi, kullanıcılarınızdan çok sayıda karmaşıklığı gizlemenize olanak tanır.
2. for
ve for_each
İfadeleriyle Koşullar
Artık count
parametresini kullanarak kaynaklarla koşul mantığını nasıl ekleyebileceğinizi anladığınıza göre, muhtemelen for_each
ifadesi kullanarak koşullu mantık yapmak adına benzer bir strateji kullanabileceğinizi tahmin edebilirsiniz. for_each
ifadesini boş bir koleksiyon iletirseniz, sonuç, for_each
'e sahip olduğunuz kaynağın, satır içi bloğun veya modülün sıfır kopyası olur; boş olmayan bir koleksiyon iletirseniz, kaynağın, satır içi bloğun veya modülün bir veya daha fazla kopyasını oluşturur. Tek soru, koleksiyonun boş olup olmamasına koşullu olarak nasıl karar verebilirsiniz?
Cevap, for_each
ifadesini for
ifadesi ile birleştirmektir. Örneğin, modules/services/webserver-cluster/main.tf
içindeki webserver-cluster
modülünün etiketleri nasıl ayarladığımızı hatırlayın:
dynamic "tag" {
for_each = var.custom_tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
var.custom_tags
boşsa, for_each
ifadesinin döngüye girecek hiçbir şeyi olmaz, bu nedenle hiçbir etiket ayarlanmaz. Başka bir deyişle, burada zaten bir koşullu mantığınız var. Ancak for_each
ifadesini aşağıdaki gibi bir for
ifadesi ile birleştirerek daha da ileri gidebilirsiniz:
dynamic "tag" {
for_each = {
for key, value in var.custom_tags:
key => upper(value)
if key != "Name"
}
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
İç içe for
ifadesi, var.custom_tags
üzerinde döngü yapar, her değeri büyük harfe dönüştürür ve modül zaten kendi Name
etiketini ayarladığından, Name
olarak ayarlanmış herhangi bir anahtarı filtrelemek için for
ifadesinde bir koşul kullanır. for
ifadesindeki değerleri filtreleyerek, isteğe bağlı koşullu mantık uygulayabilirsiniz.
Bir kaynağın veya modülün birden çok kopyasını oluşturmak için neredeyse her zaman for_each
yerine count
tercih etmeniz gerekse de, koşul söz konusu olduğunda, count
'u 0 veya 1 olarak ayarlamak, for_each
öğesini boş veya boş olmayan bir koleksiyona ayarlamaktan daha basit olma eğiliminde olduğunu unutmayın. Bu nedenle, genellikle kaynakları ve modülleri koşullu olarak oluşturmak için count
kullanılmasını ve diğer tüm döngü ve koşul türleri için for_each
öğesini kullanmanızı öneririm.
3. if
String Directive İle Koşullar
Şimdi aşağıdaki sözdizimine sahip if
string directive'e bakalım:
%{ if <CONDITION> }<TRUEVAL>%{ endif }
burada CONDITION
, bir boolean olarak değerlendirilen herhangi bir ifadedir ve TRUEVAL
, CONDITION
true
olarak değerlendirilirse oluşturulacak ifadedir.
Bu bölümün başlarında, virgülle ayrılmış isim çıktısı almak için for
string directive kullandınız. Sorun, stringin sonunda fazladan bir virgül ve boşluk olmasıydı. Bu sorunu aşağıdaki gibi çözmek için if
string yönergesini kullanabilirsiniz:
output "for_directive_index_if" {
value = <<EOF
%{ for i, name in var.names }
${name}%{ if i < length(var.names) - 1 }, %{ endif }
%{ endfor }
EOF
}
Burada orijinal sürümden birkaç değişiklik var:
- Kodu, çok satırlı stringleri tanımlamanın bir yolu olan bir
HEREDOC
'a koydum. Bu, kodu daha okunaklı olması için birkaç satıra yaymamı sağlıyor. - Listedeki son öğe için virgül ve boşluk vermemek için
if
string yönergesini kullandım.
terraform apply
çalıştırdığınızda, aşağıdaki çıktıyı alırsınız:
$ terraform apply
(...)
Outputs:
for_directive_index_if = <<EOT
ismet,
ozlem,
furkan
EOT
Vay canına. Sondaki virgül gitti, ancak bir sürü fazladan boşluk (boşluklar ve yeni satırlar) ekledik. HEREDOC'a koyduğunuz her boşluk, son dizede biter. Bunu, şerit işaretçisinden önce veya sonra fazladan boşlukları tüketecek olan dize yönergelerinize şerit işaretleri (~
) ekleyerek düzeltebilirsiniz:
output "for_directive_index_if_strip" {
value = <<EOF
%{~ for i, name in var.names ~}
${name}%{ if i < length(var.names) - 1 }, %{ endif }
%{~ endfor ~}
EOF
}
Bu sürümü bir deneyelim:
$ terraform apply
(...)
Outputs:
for_directive_index_if_strip = "ismet, ozlem, furkan"
Tamam, bu güzel bir gelişme: fazladan boşluk veya virgül yok. Aşağıdaki sözdizimini kullanan string directive için bir else
ekleyerek bu çıktıyı daha da güzel hale getirebilirsiniz:
%{ if <CONDITION> }<TRUEVAL>%{ else }<FALSEVAL>%{ endif }
burada FALSEVAL
, CONDITION
false
olarak değerlendirilirse oluşturulacak ifadedir. Sonuna nokta eklemek için else yan tümcesinin nasıl kullanılacağına dair bir örnek:
output "for_directive_index_if_else_strip" {
value = <<EOF
%{~ for i, name in var.names ~}
${name}%{ if i < length(var.names) - 1 }, %{ else }.%{ endif }
%{~ endfor ~}
EOF
}
terraform apply
çalıştırdığınızda, aşağıdaki çıktıyı alırsınız:
$ terraform apply
(...)
Outputs:
for_directive_index_if_else_strip = "ismet, ozlem, furkan."