Tworzenie Elastycznych Modułów Terraform na Przykładzie Azure AKS

Łukasz Kołodziej

DevOps i Cloud Architect

15 grudnia 2024

Jak stworzyć uniwersalne moduły w Terraform na przykładzie Azure AKS?

Tworzenie uniwersalnych modułów Terraform wymaga elastyczności, aby mogły być one łatwo wykorzystane w różnych scenariuszach. W tym artykule przedstawię podejście oparte na przykładzie modułu Azure Kubernetes Service (AKS), aby pokazać, jak budować moduły, które są zarówno elastyczne, jak i maksymalnie uniwersalne. Dzięki temu zyskasz możliwość prostego zarządzania infrastrukturą, niezależnie od środowiska, w którym pracujesz.

Skalowalność i Elastyczność dzięki for_each

Jednym z kluczowych elementów naszego modułu jest `for_each`, który pozwala definiować wiele zasobów na podstawie przekazanych zmiennych. Dzięki niemu możemy tworzyć zasoby w sposób dynamiczny i skalowalny, dopasowując ich liczbę do potrzeb.

Na przykład, aby utworzyć wiele klastrów AKS, używamy for_each do iteracji przez wszystkie definicje klastrów, które znajdują się w zmiennej var.kubernetes_clusters. To podejście pozwala zautomatyzować tworzenie zasobów bez konieczności ręcznego ich definiowania, co jest szczególnie przydatne przy dużych infrastrukturach.

resource "azurerm_kubernetes_cluster" "aks" {
  for_each = var.kubernetes_clusters
  name     = each.value.custom_name != null ? each.value.custom_name : "${var.prefix}-${each.value.name}-aks-${var.environment}-${var.region_suffix}"
  # Pozostałe właściwości...
}

Dzięki for_each, proces definiowania zasobów staje się nie tylko prostszy, ale także bardziej zautomatyzowany, co z kolei pozwala na większą skalowalność i elastyczność.

Parametryzacja Nazw Zasobów

Parametryzacja nazw to kolejny sposób, który pomaga nam tworzyć elastyczne moduły. W naszym przykładzie AKS używamy wyrażeń warunkowych, aby dynamicznie generować nazwy zasobów w zależności od wartości zmiennych. Pozwala to na większą elastyczność i spójność w zarządzaniu infrastrukturą.

name = each.value.custom_name != null ? each.value.custom_name : "${var.prefix}-${each.value.name}-aks-${var.environment}-${var.region_suffix}"

Dzięki temu moduł można łatwo dostosować do różnych środowisk (np. dev, test, prod) oraz regionów chmurowych, co znacząco ułatwia zarządzanie. Tego rodzaju elastyczność umożliwia dostosowanie zasobów do specyficznych potrzeb każdego środowiska.

Wykorzystywanie Lokalnych Tagów

Dodawanie tagów do zasobów to świetny sposób na lepszą organizację i zarządzanie infrastrukturą. W naszym module używamy lokalnych tagów, aby wzbogacić zasoby o dodatkowe informacje, takie jak nazwa modułu (`tf_module_name`) i jego wersja (`tf_module_ver`).

locals {
  tags = merge(each.value.tags, { tf_module_name = "aks", tf_module_ver = "1.0.0" })
}

resource "azurerm_kubernetes_cluster" "aks" {
  tags = merge(each.value.tags, local.tags)
}

Dzięki temu łatwiej jest zidentyfikować zasoby w panelu zarządzania chmurą i prowadzić audyt. Tagi pomagają w standaryzacji zasobów, co z kolei ułatwia ich monitorowanie oraz analizę kosztów.

Zabezpieczenia i Walidacje Danych Wejściowych

Kolejnym kluczowym aspektem naszego modułu jest walidacja danych wejściowych. W Terraform używamy null_resource wraz z for_each i provisioner „local-exec” do sprawdzenia, czy użytkownik przekazał wszystkie niezbędne parametry dla każdego klastra AKS.

resource "null_resource" "validate_aks_names" {
  for_each = var.kubernetes_clusters
  count = (each.value.name == null && lookup(each.value, "custom_name", null) == null) ? 1 : 0

  provisioner "local-exec" {
    command = "echo 'Error: Either custom_name or name must be defined for each AKS cluster.' && exit 1"
  }
}

Dzięki temu zapewniamy, że każdy klaster jest prawidłowo skonfigurowany, co zmniejsza ryzyko błędów. Walidacja pomaga w upewnieniu się, że każda konfiguracja jest zgodna ze standardami oraz że zasoby są tworzone zgodnie z oczekiwaniami.

Finalny Moduł – Elastyczność w Działaniu

W naszej konfiguracji tworzymy różne zasoby wspierające działanie AKS, takie jak dodatkowe pule węzłów, diagnostyka monitorowania oraz prywatne połączenia sieciowe. Taka architektura pozwala na kompleksowe zarządzanie klastrami, zapewniając jednocześnie elastyczność i możliwość rozszerzania modułu.

locals {
  tags = merge(each.value.tags, { tf_module_name = "aks", tf_module_ver = "1.0.0" })
}


resource "null_resource" "validate_aks_names" {
  for_each = var.kubernetes_clusters

  count = (each.value.name == null && lookup(each.value, "custom_name", null) == null) ? 1 : 0

  provisioner "local-exec" {
    command = "echo 'Error: Either custom_name or name must be defined for each AKS cluster.' && exit 1"
  }
}

resource "azurerm_kubernetes_cluster" "aks" {
  for_each            = var.kubernetes_clusters

  name                = each.value.custom_name != null ? each.value.custom_name : "${var.prefix}-${each.value.name}-aks-${var.environment}-${var.region_suffix}"
  location            = each.value.location
  resource_group_name = each.value.custom_rg_name != null ? each.value.custom_rg_name : "${var.prefix}-${each.value.name}-aks-rg-${var.environment}-${var.region_suffix}"
  kubernetes_version  = each.value.kubernetes_version
  dns_prefix          = "${each.value.name}-dns"

  default_node_pool {
    name = each.value.default_node_pool.custom_name != null ? each.value.default_node_pool.custom_name : "${var.prefix}-${each.value.default_node_pool.name}-pool${var.environment}-${var.region_suffix}"
    vm_size             = each.value.default_node_pool.vm_size
    node_count          = each.value.default_node_pool.node_count
    enable_auto_scaling = each.value.default_node_pool.enable_auto_scaling
    min_count           = each.value.default_node_pool.min_count
    max_count           = each.value.default_node_pool.max_count
  }

  identity {
    type = each.value.identity_type
  }

  network_profile {
    network_plugin     = each.value.network_profile.network_plugin
    dns_service_ip     = each.value.network_profile.dns_service_ip
   # docker_bridge_cidr = each.value.network_profile.docker_bridge_cidr
    service_cidr       = each.value.network_profile.service_cidr
  }

  tags = merge(each.value.tags, local.tags)
}

resource "azurerm_monitor_diagnostic_setting" "aks_monitoring" {
  for_each = var.monitoring_settings

  name = each.value.custom_diagnostic_name != null ? each.value.custom_diagnostic_name : "${var.prefix}-${each.value.cluster_name}-diagnostic${var.environment}-${var.region_suffix}"
  target_resource_id      = azurerm_kubernetes_cluster.aks[each.key].id
  log_analytics_workspace_id = each.value.log_analytics_workspace_id

  metric {
    category = "AllMetrics"
    enabled  = true
  }
}

resource "azurerm_kubernetes_cluster_node_pool" "additional_node_pools" {
  for_each = var.additional_node_pools

  kubernetes_cluster_id = azurerm_kubernetes_cluster.aks[each.value.cluster_key].id
  name                  = each.value.custom_node_pool_name != null ? each.value.custom_node_pool_name : "${var.prefix}-${each.key}-nodepool-${var.environment}-${var.region_suffix}"
  vm_size               = each.value.vm_size
  node_count            = each.value.node_count
  enable_auto_scaling   = each.value.enable_auto_scaling
  min_count             = each.value.min_count
  max_count             = each.value.max_count
}

resource "azurerm_subnet" "aks_subnet" {
  for_each = { for key, value in var.kubernetes_clusters : key => value if lookup(value, "create_private_endpoint", false) }

  name                 = "${var.prefix}-${each.value.name}-aks-subnet${var.environment}-${var.region_suffix}"
  resource_group_name  = each.value.vnet_resource_group_name
  virtual_network_name = each.value.virtual_network_name
  address_prefixes     = each.value.subnet_address_prefixes
}

resource "azurerm_private_endpoint" "aks_private_endpoint" {
  for_each = { for key, value in var.kubernetes_clusters : key => value if lookup(value, "create_private_endpoint", false) }

  name = each.value.custom_private_endpoint_name != null ? each.value.custom_private_endpoint_name : "${var.prefix}-${each.value.name}-aks-private-endpoint"
  location            = each.value.location
  resource_group_name = each.value.custom_rg_name != null ? each.value.custom_rg_name : "${var.prefix}-${each.value.name}-rg-${var.environment}-${var.region_suffix}"
  subnet_id           = azurerm_subnet.aks_subnet[each.key].id
  private_service_connection {
    name                           = each.value.custom_pe_name != null ? each.value.custom_pe_name : "${var.prefix}-aks-pe-${each.value.name}-${var.environment}-${var.region_suffix}"
    private_connection_resource_id = azurerm_key_vault.aks[each.key].id
    is_manual_connection           = each.value.private_endpoint.create_manual_connection
    subresource_names              = ["aks"]
  }
  tags = merge(each.value.tags, local.tags)
}

Podsumowanie – Tworzenie Elastycznego Modułu AKS

Dzięki powyższym elementom – używaniu for_each, parametryzacji nazw, tagom i walidacjom – nasz moduł AKS jest maksymalnie uniwersalny i elastyczny. Może być wykorzystany w różnych środowiskach, z różnymi konfiguracjami, zachowując pełną kontrolę nad infrastrukturą.

Dodatkowo, taka konfiguracja znacznie ułatwia import już istniejących zasobów i pozwala na ich elastyczny rozwój bez konieczności tworzenia nowych zasobów. Dzięki temu można zaoszczędzić czas oraz uniknąć ryzyka związanego z tworzeniem i konfiguracją nowych komponentów od podstaw.

Tworzenie modułów Terraform, które są elastyczne i uniwersalne, wymaga odpowiedniego planowania, ale efekty w postaci automatyzacji i minimalizacji ryzyka błędów zdecydowanie są tego warte. Elastyczność takich modułów sprawia, że mogą one być wielokrotnie wykorzystywane, co przynosi oszczędności zarówno w czasie, jak i kosztach.

Chcesz dowiedzieć się więcej o budowaniu elastycznych modułów w Terraform? Podziel się swoimi doświadczeniami lub pytaniami w komentarzach!

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *