Terraform Module Nedir? Resuable Altyapı Oluşturmak
19 min read

Terraform Module Nedir? Resuable Altyapı Oluşturmak

Terraform Module; yeniden kullanılabilir, bakımı yapılabilir ve test edilebilir Terraform kodu yazmanın temel bileşenidir.
Terraform Module Nedir? Resuable Altyapı Oluşturmak

Terraform Module; yeniden kullanılabilir, bakımı yapılabilir ve test edilebilir Terraform kodu yazmanın temel bileşenidir. Modül ile yaptığınız ortak işleri tek bir method altında toplayabilir ve kod tekrarından kurtularak daha clean, bakımı kolay ve efektif kod yazmış olursunuz.

Terraform State Nedir? Nasıl Yönetilir? başlıklı yazımızın sonunda, Şekil 1'de gösterilen mimariyi devreye almıştınız.

Şekil 1: Bir yük dengeleyici, web sunucusu kümesi ve veritabanı

Bu mimari, tek ortamınız olduğunda harika çalışır, ancak genellikle en az iki ortama ihtiyacınız vardır. Şekil 2'de gösterildiği gibi, biri ekibinizin testleri gerçekleştirdiği "stagging" ve diğeri de gerçek kullanıcıların erişebileceği "production" ortamıdır. İdeal olarak, iki ortam neredeyse aynıdır, ancak paradan tasarruf etmek için hazırlamada biraz daha az/daha küçük sunucu çalıştırabilirsiniz.

Şekil 2: Her biri kendi yük dengeleyicisine, web sunucusu kümesine ve veritabanına sahip iki ortam

Staging ortamındaki tüm kodu terraform kodunuzu kopyala yapıştır yapmak zorunda kalmadan production ortamına nasıl eklersiniz? Örneğin, stage/services/webserver-cluster içindeki tüm kodu prod/services/webserver-cluster'a ve stage/data-stores/mysql içindeki tüm kodu prod/data-stores/mysql içine kopyalayıp yapıştırmaktan nasıl kaçınırsınız?

Programlama dillerinde, aynı kodu birkaç yere kopyalayıp yapıştırdıysanız, bu kodu bir methodun içine koyabilir ve bu methodu her yerde yeniden kullanabilirsiniz:

# Tek bir yerde methodu tanımlıyoruz
def example_function():
  print("Hello, World")

# İstediğiniz her yerde bu methodu çağırabiliyoruz
example_function()

Benzer şekilde, Terraform kodunuzu bir Terraform modülünün içine koyabilir ve bu modülü kodunuz boyunca birden çok yerde yeniden kullanabilirsiniz. Staging ve production ortamlarında aynı kodu kopyalayıp yapıştırmak yerine, Şekil 3'te gösterildiği gibi her iki ortamın da aynı modülden gelen kodu yeniden kullanmasını sağlayabilirsiniz.

Şekil 3: Modüllere kod yerleştirmek, bu kodu birden çok ortamdan yeniden kullanmanıza olanak tanır

Modüller, yeniden kullanılabilir, bakımı yapılabilir ve test edilebilir Terraform kodu yazmanın temel bileşenidir. Onları kullanmaya başladığınızda, bir daha bırakamayacaksınız. Her şeyi bir modül olarak oluşturmaya, şirketinizde paylaşmak için bir modül kütüphanesi oluşturmaya, çevrimiçi bulduğunuz modülleri kullanarak ve tüm altyapınızı yeniden kullanılabilir modüller koleksiyonu olarak düşünmeye başlayacaksınız.

Bu bölümde, aşağıdaki konuları ele alarak Terraform modüllerinin nasıl oluşturulacağını ve kullanılacağını göstereceğiz:

  • Modül temelleri
  • Modül girdi değişkenleri - parametreler
  • Modül yerel değişkenleri - locals
  • Modül çıktı değişkenleri - output
  • Modül hatalar
  • Modül versiyonlama

1. Modül Temelleri

Bir Terraform modülü çok basittir ve bir klasördeki herhangi bir Terraform konfigürasyon dosyası bir modüldür. Şimdiye kadar yazdığınız tüm konfigürasyonlar, onları doğrudan dağıttığınız için özellikle ilginç olmasalar da teknik olarak modül olarak adlandırılırlar. Doğrudan bir modül üzerinde apply komutunu çalıştırırsanız, buna root modül denir. Modüllerin gerçekten neler yapabildiğini görmek için, diğer modüllerde kullanılması amaçlanan bir modül olan yeniden kullanılabilir bir modül oluşturmanız gerekir.

Örnek olarak, bir Auto Scaling Group (ASG), Application Load Balancer (ALB), güvenlik grupları ve daha birçok kaynağı içeren stage/services/webserver-cluster içindeki kodu yeniden kullanılabilir bir modüle çevirelim.

İlk adım olarak, daha önce oluşturduğunuz kaynakları temizlemek için stage/services/webserver-cluster da terraform destroy çalıştırın. Ardından, modules adlı yeni bir üst düzey klasör oluşturun ve tüm dosyaları stage/services/webserver-cluster dan modules/services/webserver-cluster a taşıyın. Şekil 4'e benzeyen bir klasör yapısı elde etmelisiniz.

main.tf dosyasını modules/services/webserver-cluster da açın ve provider tanımını kaldırın. Sağlayıcılar, modülün kendisi tarafından değil, modülün kullanıcısı tarafından yapılandırılmalıdır (sağlayıcıların nasıl ele alınacağı hakkında daha fazla bilgiyi ilerideki yazılarımızda edineceksiniz).

Şekil 4: Modül ve staging ortamı içeren klasör yapısı

Artık bu modülü staging ortamında kullanabilirsiniz. Modül kullanmanın sözdizimi şu şekildedir:

module "<NAME>" {
  source = "<SOURCE>"

  [CONFIG ...]
}
  • NAME, bu modüle başvurmak için Terraform kodu boyunca kullanabileceğiniz bir tanımlayıcıdır (ör. web-server),
  • SOURCE modül kodunun bulunabileceği yoldur (ör. modules/services/webserver-cluster)
  • CONFIG o modüle özgü bir veya daha fazla argümandan oluşur.

Örneğin, stage/services/webserver-cluster/main.tf'de yeni bir dosya oluşturabilir ve içindeki webserver-cluster modülünü aşağıdaki gibi kullanabilirsiniz:

provider "aws" {
  region = "us-east-2"
}

module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"
}

Ardından, aşağıdaki içeriklere sahip yeni bir prod/services/webserver-cluster/main.tf dosyası oluşturarak aynı modülü production ortamında yeniden kullanabilirsiniz:

provider "aws" {
  region = "us-east-2"
}

module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"
}

Ve işte karşınızda, minimum kopyalama / yapıştırma içeren ve birden çok ortamda aynı kodun yeniden kullanımını sağlayan modül yeteneği. Terraform yapılandırmalarınıza bir modül eklediğinizde veya bir modülün kaynak parametresini değiştirdiğinizde, plan veya apply komutlarını çalıştırmadan önce init komutunu çalıştırmanız gerektiğini unutmayın:

$ terraform init
Initializing modules...
- webserver_cluster in ../../../modules/services/webserver-cluster

Initializing the backend...

Initializing provider plugins...

Terraform has been successfully initialized!

Artık init komutunun içerdiği tüm hileleri gördünüz: tek bir kullanışlı komutta sağlayıcıları kurar, arka uçlarınızı yapılandırır ve modülleri indirir.

Bu kodda apply komutunu çalıştırmadan önce, webserver-cluster modülünde bir sorun olduğunu unutmayın: Tüm adlar sabit kodlanmıştır. Yani güvenlik gruplarının adı, ALB ve diğer kaynakların tümü sabit kodlanmıştır, bu nedenle bu modülü aynı AWS hesabında birden fazla kullanırsanız, ad çakışması hataları alırsınız. modules/services/webserver-cluster içine kopyaladığınız main.tf dosyası, veritabanı adresini ve bağlantı noktasını bulmak için bir terraform_remote_state veri kaynağı kullandığından ve terraform_remote_state'e bakmak için kodlanmış olduğundan, veritabanı durumunun nasıl okunacağına ilişkin ayrıntılar bile sabit kodlanmıştır.

Bu sorunları gidermek ve farklı ortamlarda farklı davranabilmesi için webserver-cluster modülünü yapılandırılabilir girdiler eklemeniz gerekir.

2. Modül Girdi Değişkenleri - Parametreler

Python gibi genel amaçlı bir programlama dilinde bir methodu yapılandırılabilir hale getirmek için bu methoda parametreler ekleyebilirsiniz:

# İki parametre alan bir method
def example_function(param1, param2)
  print("Hello", param1, param2")

# Methoda iki parametre değeri geçiyoruz
example_function("foo", "bar")

Terraform'da modüller de benzer şekilde parametrelere sahip olabilir. Bu bölümde, daha önceki "Terraform İle AWS Kullanımı ve Load Balancer Deploy" başlıklı yazımızda yaptığımız örneği modüle çevireceğiz.

Terraform İle AWS Kullanımı ve Load Balancer Deploy
Terraform ile AWS kullanarak yüksek düzeyde kullanılabilir ve ölçeklenebilir bir Load Balancer deploy edebilirsiniz.

Örneğimize başlamak için modules/services/webserver-cluster/variables.tf dosyasını açın ve üç yeni değişkeni ekleyin:

variable "cluster_name" {
  description = "The name to use for all the cluster resources"
  type        = string
}

variable "db_remote_state_bucket" {
  description = "The name of the S3 bucket for the database's remote state"
  type        = string
}

variable "db_remote_state_key" {
  description = "The path for the database's remote state in S3"
  type        = string
}

Ardından, modules/services/webserver-cluster/main.tf'e gidin ve sabit kodlanmış adlar yerine var.cluster_name kullanın (örneğin, "terraform-asg-example" yerine). Örneğin, ALB güvenlik grubu için bunu nasıl yapacağınız aşağıda gösterilmiştir:

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

name parametresinin nasıl "${var.cluster_name}-alb" olarak ayarlandığına dikkat edin. Diğer aws_security_group kaynağında (ör. ona "${var.cluster_name}-instance" adını verin), aws_alb kaynağında ve aws_autoscaling_group kaynağının etiket bölümünde benzer bir değişiklik yapmanız gerekir.

State dosyasını doğru ortamdan okuduğunuzdan emin olmak için, sırasıyla db_remote_state_bucket ve db_remote_state_key'i bucket ve key parametresi olarak kullanmak için terraform_remote_state veri kaynağını da güncellemelisiniz:

data "terraform_remote_state" "db" {
  backend = "s3"

  config = {
    bucket = var.db_remote_state_bucket
    key    = var.db_remote_state_key
    region = "us-east-2"
  }
}

Şimdi, staging ortamında, stage/services/webserver-cluster/main.tf'de, bu yeni değişkenlerini buna göre ayarlayabilirsiniz:

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"
}

Bu değişkenleri ayrıca prod/services/webserver-cluster/main.tf içindeki production ortamında, ancak o ortama karşılık gelen farklı değerlere ayarlamalısınız:

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"
}

Gördüğünüz gibi, bir kaynak için argümanları ayarlamakla aynı sözdizimini kullanarak bir modül için değişkenlerini ayarlarsınız. Değişkenler, modülün farklı ortamlarda nasıl davranacağını kontrol eden API'dir.

Şimdiye kadar name ve database remote state için girdi değişkenleri eklediniz, ancak diğer parametreleri de modülünüzde yapılandırılabilir hale getirmek isteyebilirsiniz. Örneğin, staging ortamında paradan tasarruf etmek için küçük bir web sunucusu kümesi çalıştırmak isteyebilirsiniz, ancak üretimde çok fazla trafiği yönetmek için daha büyük bir küme çalıştırmak isteyebilirsiniz. Bunu yapmak için module/services/webserver-cluster/variables.tf dosyasına üç girdi değişkeni daha ekleyebilirsiniz:

variable "instance_type" {
  description = "The type of EC2 Instances to run (e.g. t2.micro)"
  type        = string
}

variable "min_size" {
  description = "The minimum number of EC2 Instances in the ASG"
  type        = number
}

variable "max_size" {
  description = "The maximum number of EC2 Instances in the ASG"
  type        = number
}

Ardından, instance_type parametresini yeni var.instance_type giriş değişkenine ayarlamak için module/services/webserver-cluster/main.tf içindeki başlatma yapılandırmasını güncelleyin:

resource "aws_launch_configuration" "example" {
  image_id        = "ami-0fb653ca2d3203ac1"
  instance_type   = var.instance_type
  security_groups = [aws_security_group.instance.id]

  user_data = templatefile("user-data.sh", {
    server_port = var.server_port
    db_address  = data.terraform_remote_state.db.outputs.address
    db_port     = data.terraform_remote_state.db.outputs.port
  })

  # Required when using a launch configuration with an auto scaling group.
  lifecycle {
    create_before_destroy = true
  }
}

Benzer şekilde, min_size ve max_size parametrelerini sırasıyla yeni var.min_size ve var.max_size girdi değişkenlerine ayarlamak için aynı dosyadaki ASG tanımını güncellemelisiniz:

resource "aws_autoscaling_group" "example" {
  launch_configuration = aws_launch_configuration.example.name
  vpc_zone_identifier  = data.aws_subnets.default.ids
  target_group_arns    = [aws_lb_target_group.asg.arn]
  health_check_type    = "ELB"

  min_size = var.min_size
  max_size = var.max_size

  tag {
    key                 = "Name"
    value               = var.cluster_name
    propagate_at_launch = true
  }
}

Şimdi, staging ortamında (stage/services/webserver-cluster/main.tf), instance_type öğesini "t2.micro" ve min_size ve max_size öğesini 2 olarak ayarlayarak kümeyi küçük ve ucuz tutabilirsiniz:

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
}

Öte yandan, prodcution ortamında, m4.large gibi daha fazla CPU ve belleğe sahip daha büyük bir instance_type kullanabilirsiniz (bu Instance türünün AWS ücretsiz katmanının bir parçası olmadığını unutmayın. Bu nedenle yalnızca öğrenmek için kullanıyor ve ücretlendirilmek istemiyorsanız, instance_type için "t2.micro" ya bağlı kalın) ve kümenin yüke bağlı olarak küçülmesine veya büyümesine izin vermek için max_size değerini 10 olarak ayarlayabilirsiniz (endişelenmeyin, küme başlangıçta iki Instance ile başlatılır):

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
}

3. Modül Yerel Değişkenler - Locals

Modülünüzün girdilerini tanımlamak için giriş değişkenlerini kullanmak harikadır, ancak bazı ara hesaplamalar yapmak veya yalnızca kodunuzu DRY tutmak için modülünüzde bir değişken tanımlamanın bir yoluna ihtiyacınız varsa, ancak bu değişkeni yapılandırılabilir bir giriş olarak göstermek istemiyorsanız ne olur? Örneğin, module/services/webserver-cluster/main.tf'deki webserver-cluster modülündeki yük dengeleyici, HTTP için varsayılan bağlantı noktası olan 80 numaralı bağlantı noktasını dinler. Bu bağlantı noktası numarası şu anda yük dengeleyici dinleyicisi de dahil olmak üzere birden çok yere kopyalanıp yapıştırılmıştır:

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.example.arn
  port              = 80
  protocol          = "HTTP"

  # By default, return a simple 404 page
  default_action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/plain"
      message_body = "404: page not found"
      status_code  = 404
    }
  }
}

Ve yük dengeleyici güvenlik grubu:

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

"Tüm IP'ler" CIDR bloğu 0.0.0.0/0, "any port" değeri 0 ve "any protokol" değeri "-1" de dahil olmak üzere güvenlik grubundaki değerler modül boyunca çeşitli yerlere kopyalanır ve yapıştırılır. Bu sihirli değerlerin birden çok yerde sabit kodlanmış olması, kodun okunmasını ve korunmasını zorlaştırır. Değerleri giriş değişkenler olarak ayarlayabilirsiniz, ancak daha sonra modülünüzün kullanıcıları (yanlışlıkla) bu değerleri geçersiz kılabilir ve bu durumu istemeyebilirsiniz. Giriş değişkenlerini kullanmak yerine, bunları bir locals bloğunda yerel değişkenler olarak tanımlayabilirsiniz:

locals {
  http_port    = 80
  any_port     = 0
  any_protocol = "-1"
  tcp_protocol = "tcp"
  all_ips      = ["0.0.0.0/0"]
}

Yerel değerler, herhangi bir Terraform ifadesine bir ad atamanıza ve bu adı modül boyunca kullanmanıza olanak tanır. Bu adlar yalnızca modül içinde görünür, bu nedenle diğer modüller üzerinde hiçbir etkileri olmaz ve bu değerleri modülün dışından geçersiz kılamazsınız. Bir yerelin değerini okumak için aşağıdaki sözdizimini kullanan bir yerel başvuru kullanmanız gerekir:

local.<NAME>

Load balancer listener bağlantı noktası parametresini güncellemek için artık bu söz dizimini kullanabilirsiniz:

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.example.arn
  port              = local.http_port
  protocol          = "HTTP"

  # By default, return a simple 404 page
  default_action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/plain"
      message_body = "404: page not found"
      status_code  = 404
    }
  }
}

Yük dengeleyici güvenlik grubu da dahil olmak üzere modüldeki güvenlik gruplarındaki hemen hemen tüm parametreleri güncelleyebilirsiniz:

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"

  ingress {
    from_port   = local.http_port
    to_port     = local.http_port
    protocol    = local.tcp_protocol
    cidr_blocks = local.all_ips
  }

  egress {
    from_port   = local.any_port
    to_port     = local.any_port
    protocol    = local.any_protocol
    cidr_blocks = local.all_ips
  }
}

Locals, kodunuzun okunmasını ve bakımını kolaylaştırır, bu nedenle bunları sık sık kullanın.

4. Modül Çıktı Değişkenleri - Output

ASG'lerin güçlü bir özelliği, onları yüke yanıt olarak çalıştırdığınız sunucuların sayısını artıracak veya azaltacak şekilde yapılandırabilmenizdir. Bunu yapmanın bir yolu, gün içinde planlanan bir zamanda kümenin boyutunu değiştirebilen zamanlanmış bir eylem kullanmaktır. Örneğin, normal iş saatlerinde kümenize gelen trafik çok daha yüksekse, sunucu sayısını sabah 9'da artırmak ve akşam 5'te azaltmak için zamanlanmış bir eylem kullanabilirsiniz.

webserver-cluster modülünde zamanlanmış eylemi tanımlarsanız, hem staging hem de prdocution için geçerli olacaktır. Staging ortamınızda bu tür bir ölçekleme yapmanız gerekmediğinden, şimdilik, otomatik ölçeklendirme çizelgesini doğrudan production konfigürasyonlarında tanımlayabilirsiniz.

Zamanlanmış bir eylem tanımlamak için aşağıdaki iki aws_autoscaling_schedule kaynağını prod/services/webserver-cluster/main.tf dosyasına ekleyin:

resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
  scheduled_action_name = "scale-out-during-business-hours"
  min_size              = 2
  max_size              = 10
  desired_capacity      = 10
  recurrence            = "0 9 * * *"
}

resource "aws_autoscaling_schedule" "scale_in_at_night" {
  scheduled_action_name = "scale-in-at-night"
  min_size              = 2
  max_size              = 10
  desired_capacity      = 2
  recurrence            = "0 17 * * *"
}

Bu kod, sabah saatlerinde sunucu sayısını 10'a çıkarmak için bir aws_autoscaling_schedule kaynağı (recurrence parametresi cron sözdizimini kullanır, dolayısıyla "0 9 * * *", "her gün sabah 9" anlamına gelir) ve gece sunucu sayısını azaltmak için ikinci bir aws_autoscaling_schedule kaynağı ("0 17 * * *", "her gün 17:00" anlamına gelir) kullanır. Ancak, her iki aws_autoscaling_schedule kullanımında, ASG'nin adını belirten autoscaling_group_name adlı gerekli bir parametre eksik. ASG'nin kendisi webserver-cluster modülü içinde tanımlanır, peki onun adına nasıl erişirsiniz? Python gibi genel amaçlı bir programlama dilinde fonksiyonlar değerler döndürebilir:

# A function that returns a value
def example_function(param1, param2):
  return "Hello," param1, param2

# Call the function and get the return value
return_value = example_function("foo", "bar")

Terraform'da bir modül de değerler döndürebilir. Yine, bunu zaten bildiğiniz bir mekanizma kullanarak yaparsınız: çıktı değişkenleri. ASG adını /modules/services/webserver-cluster/outputs.tf dosyasına aşağıdaki gibi bir çıktı değişkeni olarak ekleyebilirsiniz:

output "asg_name" {
  value       = aws_autoscaling_group.example.name
  description = "The name of the Auto Scaling Group"
}

Modül çıktı değişkenlerine aşağıdaki sözdizimini kullanarak erişebilirsiniz:

module.<MODULE_NAME>.<OUTPUT_NAME>

Örneğin:

module.frontend.asg_name

prod/services/webserver-cluster/main.tf dosyasında, aws_autoscaling_schedule kaynaklarının her birinde autoscaling_group_name parametresini ayarlamak için bu söz dizimini kullanabilirsiniz:

resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
  scheduled_action_name = "scale-out-during-business-hours"
  min_size              = 2
  max_size              = 10
  desired_capacity      = 10
  recurrence            = "0 9 * * *"

  autoscaling_group_name = module.webserver_cluster.asg_name
}

resource "aws_autoscaling_schedule" "scale_in_at_night" {
  scheduled_action_name = "scale-in-at-night"
  min_size              = 2
  max_size              = 10
  desired_capacity      = 2
  recurrence            = "0 17 * * *"

  autoscaling_group_name = module.webserver_cluster.asg_name
}

webserver-cluster modülünde başka bir çıktı olan ALB'nin DNS adını çıktı olarak eklemek isteyebilirsiniz. Böylece küme dağıtıldığında hangi URL'nin test edileceğini bilirsiniz. Bunu yapmak için /modules/services/webserver-cluster/outputs.tf dosyasına tekrar bir çıktı değişkeni eklersiniz:

output "alb_dns_name" {
  value       = aws_lb.example.dns_name
  description = "The domain name of the load balancer"
}

Daha sonra bu çıktıyı stage/services/webserver-cluster/outputs.tf ve prod/services/webserver-cluster/outputs.tf dosyasında aşağıdaki gibi geçebilirsiniz:

output "alb_dns_name" {
  value       = module.webserver_cluster.alb_dns_name
  description = "The domain name of the load balancer"
}

Web sunucusu kümeniz dağıtıma neredeyse hazır. Geriye kalan tek şey, birkaç yanlışı hesaba katmak.

5. Modül Hatalar

Modülleri oluştururken şu hatalara dikkat etmelisiniz:

  • Dosya yolları
  • Satıriçi Bloklar

5.1. Dosya Yolları

Terraform State Nedir? Nasıl Yönetilir? başlıklı yazımızda, User Data komut dosyasını user-data.sh adlı harici bir dosyaya taşıdınız ve bu dosyayı diskten okumak için templatefile işlevini kullandınız. templatefile işleviyle ilgili önemli olan, kullandığınız dosya yolunun relative bir yol olması gerektiğidir (Terraform kodunuz her biri farklı bir disk düzenine sahip birçok farklı bilgisayarda çalışabileceğinden absolute dosya yollarını kullanmak istemezsiniz)—ama ne relative path nedir?

Varsayılan olarak, Terraform yolu mevcut çalışma dizinine göre yorumlar. Bu, terraform apply'ı çalıştırdığınız dizinle aynı dizinde bulunan bir Terraform yapılandırma dosyasında templatefile işlevini kullanıyorsanız (yani, kök modülde templatefile işlevini kullanıyorsanız) işe yarar, ancak ayrı bir klasörde (yeniden kullanılabilir bir modül) tanımlanan bir modülde templatefile kullanırken çalışmaz.

Bu sorunu çözmek için, path referansı olarak bilinen ve path.<TYPE> biçimindeki bir ifadeyi kullanabilirsiniz. Terraform, aşağıdaki yol referansı türlerini destekler:

  • path.module: İfadenin tanımlandığı modülün dosya sistemi yolunu döndürür.
  • path.root: Kök modülün dosya sistemi yolunu döndürür.
  • path.cwd: Geçerli çalışma dizininin dosya sistemi yolunu döndürür. Terraform'un normal kullanımında bu, path.root ile aynıdır, ancak Terraform'un bazı gelişmiş kullanımları, onu kök modül dizini dışında bir dizinden çalıştırarak bu yolların farklı olmasına neden olur.

User Data script dosyası için, modülün kendisine göre bir yola ihtiyacınız vardır, bu nedenle modules/services/webserver-cluster/main.tf'de templatefile işlevini çağırırken path.module kullanmalısınız:

user_data = templatefile("${path.module}/user-data.sh", {
    server_port = var.server_port
    db_address  = data.terraform_remote_state.db.outputs.address
    db_port     = data.terraform_remote_state.db.outputs.port
})

5.2. Satıriçi Bloklar

Bazı Terraform kaynakları için yapılandırma, satır içi bloklar veya ayrı kaynaklar olarak tanımlanabilir. Satır içi blok, şu biçimdeki bir kaynak içinde ayarladığınız bir argümandır:

resource "xxx" "yyy" {
  <NAME> {
    [CONFIG...]
  }
}

burada NAME, satır içi bloğun adıdır (örneğin, ingress) ve CONFIG, o satır içi bloğa özgü bir veya daha fazla bağımsız değişkenden oluşur (örneğin, from_port ve to_port). Örneğin, aws_security_group_resource ile, satır içi blokları (ör. ingress { … }) veya ayrı aws_security_group_rule kaynaklarını kullanarak giriş ve çıkış kurallarını tanımlayabilirsiniz.

Terraform'un nasıl tasarlandığından dolayı hem satır içi blokların hem de ayrı kaynakların bir karışımını kullanmaya çalışırsanız, yapılandırmaların çakıştığı ve birbirinin üzerine yazdığı yerlerde hatalar alırsınız. Bu nedenle, birini veya diğerini kullanmalısınız. Tavsiyemi sorarsanız: Modül oluştururken her zaman ayrı kaynaklar kullanmayı tercih etmelisiniz.

Ayrı kaynaklar kullanmanın avantajı, bunların herhangi bir yere eklenebilmesidir, oysa satır içi blok yalnızca kaynak oluşturan modül içinde eklenebilir. Bu nedenle, yalnızca ayrı kaynakları kullanmak, modülünüzü daha esnek ve yapılandırılabilir hale getirir.

Örneğin, webserver-cluster modülünde (modules/services/webserver-cluster/main.tf), ingress ve egress kurallarını tanımlamak için satır içi blokları kullandık:

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"

  ingress {
    from_port   = local.http_port
    to_port     = local.http_port
    protocol    = local.tcp_protocol
    cidr_blocks = local.all_ips
  }

  egress {
    from_port   = local.any_port
    to_port     = local.any_port
    protocol    = local.any_protocol
    cidr_blocks = local.all_ips
  }
}

Bu satır içi bloklarla, bu modülün bir kullanıcısının modülün dışından ek ingress ve egress kuralları ekleme yolu yoktur. Modülünüzü daha esnek hale getirmek için, ayrı aws_security_group_rule kaynakları kullanarak tam olarak aynı ingress ve egress kurallarını tanımlayacak şekilde değiştirmelisiniz (bunu modüldeki her iki güvenlik grubu için yaptığınızdan emin olun):

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"
}

resource "aws_security_group_rule" "allow_http_inbound" {
  type              = "ingress"
  security_group_id = aws_security_group.alb.id

  from_port   = local.http_port
  to_port     = local.http_port
  protocol    = local.tcp_protocol
  cidr_blocks = local.all_ips
}

resource "aws_security_group_rule" "allow_all_outbound" {
  type              = "egress"
  security_group_id = aws_security_group.alb.id

  from_port   = local.any_port
  to_port     = local.any_port
  protocol    = local.any_protocol
  cidr_blocks = local.all_ips
}

Ayrıca aws_security_group kimliğini modules/services/webserver-cluster/outputs.tf'de bir çıktı değişkeni olarak dışa aktarmalısınız:

output "alb_security_group_id" {
  value       = aws_security_group.alb.id
  description = "The ID of the Security Group attached to the load balancer"
}

Şimdi, yalnızca staging ortamında (örneğin, test için) fazladan bir bağlantı noktası göstermeniz gerekiyorsa, bunu stage/services/webserver-cluster/main.tf'ye bir aws_security_group_rule kaynağı ekleyerek yapabilirsiniz:

module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"

  # (parameters hidden for clarity)
}

resource "aws_security_group_rule" "allow_testing_inbound" {
  type              = "ingress"
  security_group_id = module.webserver_cluster.alb_security_group_id

  from_port   = 12345
  to_port     = 12345
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

Satır içi blok olarak tek bir ingress veya egress kuralı tanımlamış olsaydınız, bu kod çalışmazdı. Bu aynı tür sorunun aşağıdakiler gibi bir dizi Terraform kaynağını etkilediğini unutmayın:

  • aws_security_group ve aws_security_group_rule
  • aws_route_table ve aws_route
  • aws_network_acl ve aws_network_acl_rule

Bu noktada, web sunucusu kümenizi hem staging hem de production ortamında dağıtmaya nihayet hazırsınız. Terraform uygulamasını her zamanki gibi çalıştırın ve altyapınızın iki ayrı kopyasını kullanmanın keyfini çıkarın.

6. Modül Versiyonlama

Hem staging hem de production ortamınız aynı modül klasörünü işaret ediyorsa, o klasörde değişiklik yaptığınız anda bu, bir sonraki dağıtımda her iki ortamı da etkiler. Bu tür bir bağlantı, production'ı etkileme şansı olmadan bir değişikliği test etmeyi daha da zorlaştırır. Daha iyi bir yaklaşım olarak, Şekil 5'te gösterildiği gibi, staging'de bir sürümü (örn., v0.0.2) ve production'da farklı bir sürümü (örn. v0.0.1) kullanabilmeniz için sürümlü modüller oluşturmaktır.

Şekil 5: Bir modülün farklı versiyonlarını farklı ortamlarda kullanma

Şimdiye kadar gördüğünüz tüm modül örneklerinde, ne zaman bir modül kullansanız, modülün kaynak parametresini yerel bir dosya yoluna ayarlarsınız. Dosya yollarına ek olarak, Terraform Git URL'leri, Mercurial URL'leri ve isteğe bağlı HTTP URL'leri gibi diğer modül kaynakları türlerini destekler. Sürümlü bir modül oluşturmanın en kolay yolu, modülün kodunu ayrı bir Git deposuna koymak ve kaynak parametreyi bu reponun URL'sine ayarlamaktır. Bu, Terraform kodunuzun (en az) iki repoya yayılacağı anlamına gelir:

  • modules: Bu repo, yeniden kullanılabilir modülleri tanımlar. Her modülü, altyapınızın belirli bir bölümünü tanımlayan bir "blueprint" olarak düşünün.
  • live: Bu repo, her ortamda (stage, prod, mgmt, vb.) çalıştırdığınız canlı altyapıyı tanımlar. Bunu, modül deposundaki “blueprints” lerden inşa ettiğiniz “evler” olarak düşünün.

Terraform kodunuz için güncellenmiş klasör yapısı şimdi Şekil 6'ya benziyor.

Şekil 6: Birden çok repoya sahip dosya düzeni

Bu klasör yapısını kurmak için önce stage, prod ve global klasörlerini live adlı bir klasöre taşımanız gerekir. Ardından, live ve modules klasörlerini ayrı Git repoları olarak yapılandırın. Modüller klasörü için bunun nasıl yapılacağına dair bir örnek:

$ cd modules
$ git init
$ git add .
$ git commit -m "Initial commit of modules repo"
$ git remote add origin "(URL OF REMOTE GIT REPOSITORY)"
$ git push origin main

Sürüm numarası olarak kullanmak için modül deposuna bir etiket de ekleyebilirsiniz. GitHub kullanıyorsanız, başlık altında bir etiket oluşturacak bir sürüm oluşturmak için GitHub kullanıcı arayüzünü kullanabilirsiniz. GitHub kullanmıyorsanız Git CLI'yi kullanabilirsiniz:

$ git tag -a "v0.0.1" -m "First release of webserver-cluster module"
$ git push --follow-tags

Artık bu sürümlü modülü, kaynak parametrede bir Git URL'si belirterek hem staging hem de production ortamda kullanabilirsiniz. Modül reponuz GitHub deposunda olsaydı, live/stage/services/webserver-cluster/main.tf dosyasında bu şöyle görünürdü (aşağıdaki Git URL'sindeki çift slash gereklidir):

module "webserver_cluster" {
  source = "github.com/foo/modules//webserver-cluster?ref=v0.0.1"

  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
}

ref parametresi, sha1 hash, branch adı veya bu örnekte olduğu gibi belirli bir Git etiketi aracılığıyla belirli bir Git commiti belirtmenize olanak tanır. Modüllerin sürüm numaraları olarak genellikle Git etiketlerini kullanmanızı öneririm. Branch adları sabit değildir, çünkü bir branch'te her zaman en son commiti alırsınız. Bu init komutunu her çalıştırdığınızda değişebilir ve sha1 hashleri ise pek insan dostu değildir. Git etiketleri bir commit kadar kararlıdır (aslında, bir etiket yalnızca bir commit için bir işaretçidir), ancak kolay, okunabilir bir ad kullanmanıza izin verir.

Etiketler için özellikle yararlı bir adlandırma şeması semantik sürüm oluşturmadır. Bu, sürüm numarasının her bir bölümünü ne zaman artırmanız gerektiğine ilişkin belirli kurallarla birlikte MAJOR.MINOR.PATCH (örn. 1.0.4) biçiminin bir sürüm oluşturma şemasıdır. Özellikle, şunları artırmalısınız:

  • MAJOR sürümü, uyumsuz API değişiklikleri yaptığınızda,
  • MINOR sürüm, geriye dönük uyumlu bir şekilde işlevsellik eklediğinizde,
  • PATCH sürümü, geriye dönük uyumlu hata düzeltmeleri yaptığınızda

Semantik sürüm oluşturma, modülünüzün kullanıcılarına ne tür değişiklikler yaptığınızı ve yükseltmenin sonuçlarını bildirmeniz için bir yol sağlar.

Terraform kodunuzu sürümlü bir modül URL'si kullanacak şekilde güncellediğiniz için, Terraform'a terraform init'i yeniden çalıştırarak modül kodunu indirmesi için talimat vermeniz gerekir:

$ terraform init
Initializing modules...
Downloading [email protected]:brikis98/terraform-up-and-running-code.git?ref=v0.2.0
for webserver_cluster...

(...)

Bu sefer Terraform'un modül kodunu yerel dosya sisteminizden ziyade Git'ten indirdiğini görebilirsiniz. Modül kodu indirildikten sonra, apply komutunu her zamanki gibi çalıştırabilirsiniz.

7. ÖZET

Modüllerde altyapıyı kod olarak tanımlayarak, çeşitli yazılım mühendisliği best practiceleri altyapınıza uygulayabilirsiniz. Kod reviewler ve otomatik testler yoluyla bir modülde yapılan her değişikliği doğrulayabilirsiniz. Her modülün anlamsal olarak sürümlendirilmiş sürümlerini oluşturabilirsiniz ve bir modülün farklı sürümlerini farklı ortamlarda güvenle deneyebilir ve bir sorunla karşılaşırsanız önceki sürümlere geri dönebilirsiniz.

Tüm bunlar, altyapıyı hızlı ve güvenilir bir şekilde oluşturma yeteneğinizi önemli ölçüde artırabilir, çünkü geliştiriciler kanıtlanmış, test edilmiş ve belgelenmiş altyapının tüm parçalarını yeniden kullanabilecektir. Örneğin, bir kümenin nasıl çalıştırılacağı, kümenin yüke yanıt olarak nasıl ölçeklendirileceği ve trafik isteklerinin küme genelinde nasıl dağıtılacağı dahil olmak üzere tek bir mikro hizmetin nasıl dağıtılacağını tanımlayan kurallı bir modül oluşturabilirsiniz ve her ekip bunu kullanabilir. Bu modül, kendi mikro hizmetlerini yalnızca birkaç satır kodla yönetir.

Böyle bir modülün birden fazla ekip için çalışmasını sağlamak için, o modüldeki Terraform kodunun esnek ve yapılandırılabilir olması gerekir. Örneğin, bir ekip, yük dengeleyici olmadan mikro hizmetlerinin tek bir Instance dağıtmak için modülünüzü kullanmak isteyebilirken, bir başka ekip, trafiği bu Instance'lar arasında dağıtmak için bir yük dengeleyici ile mikro hizmetlerinin bir düzine Instance'ını isteyebilir.