Terraform If/Else Koşullar (Conditionals)
10 min read

Terraform If/Else Koşullar (Conditionals)

Terraform If/Else koşul ifadelerini nasıl kullacağınızı detaylı örneklerle gösteriyoruz.
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ır
  • for_each ve for ifadeleri: Bir kaynak içindeki koşullu kaynaklar ve satır içi bloklar için kullanılır
  • if 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özdizimi CONDITION içindeki Boolean mantığını değerlendirecek ve eğer sonuç true ise TRUE_VAL, sonuç false ise FALSE_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."