Skip to content

Latest commit

Β 

History

History
337 lines (255 loc) Β· 7.48 KB

File metadata and controls

337 lines (255 loc) Β· 7.48 KB

Terraform Dependency Patterns - Best Practices

This document outlines resilient dependency patterns to prevent Terraform failures when services are enabled/disabled or scaled.

⚠️ Critical Issues & Solutions

Issue 1: Hardcoded [0] References in depends_on

Problem:

resource "kubernetes_stateful_set" "my_service" {
  count = var.my_service_enabled ? 1 : 0

  depends_on = [
    null_resource.some_resource[0],  # ❌ BREAKS if count = 0
  ]
}

When null_resource.some_resource has count = 0, there is no [0] instance, causing Terraform to fail.

Solution:

resource "kubernetes_stateful_set" "my_service" {
  count = var.my_service_enabled ? 1 : 0

  depends_on = [
    null_resource.some_resource,  # βœ… Works with count = 0 or count = 1
  ]
}

Terraform automatically handles resources with count in depends_on:

  • If count = 0: Dependency is ignored (resource doesn't exist)
  • If count = 1: Waits for the resource normally
  • Never use [0] in depends_on - reference the resource directly

Issue 2: Static Lists of Conditional Resources

Problem:

resource "null_resource" "cleanup" {
  depends_on = [
    kubernetes_stateful_set.service_a,  # Might have count = 0
    kubernetes_stateful_set.service_b,  # Might have count = 0
    kubernetes_stateful_set.service_c,  # Might have count = 0
  ]
}

This creates brittle dependencies. If services are enabled/disabled, the dependency tree doesn't adapt.

Solution A Remove Unnecessary Dependencies

resource "null_resource" "cleanup" {
  # No dependencies - script handles missing services gracefully

  provisioner "local-exec" {
    # Script checks if services exist before processing
    command = "python cleanup.py"
  }
}

Solution B Use Retry Logic

resource "null_resource" "update_dns" {
  provisioner "local-exec" {
    command = <<-EOT
      for i in {1..30}; do
        if python update_dns.py; then
          exit 0
        fi
        echo "Waiting for service to be ready..."
        sleep 10
      done
    EOT
  }
}

Issue 3: Cross-Service Dependencies

Problem:

resource "kubernetes_stateful_set" "service_a" {
  count = var.service_a_enabled ? 1 : 0

  depends_on = [
    kubernetes_stateful_set.service_b[0],  # ❌ Breaks if service_b disabled
  ]
}

Solution: Remove [0] and Add Comments

resource "kubernetes_stateful_set" "service_a" {
  count = var.service_a_enabled ? 1 : 0

  depends_on = [
    # Conditional dependency - ignored if service_b_enabled = false
    # Note: service_a requires service_b to be enabled for full functionality
    kubernetes_stateful_set.service_b,  # βœ… Safe with count = 0
  ]
}

βœ… Recommended Patterns

Pattern 1: Self-Contained Services

Each service should be independently deployable:

resource "kubernetes_stateful_set" "my_service" {
  count = var.my_service_enabled ? 1 : 0

  depends_on = [
    # Core infrastructure only
    null_resource.wait_for_namespace,
    kubectl_manifest.wildcard_cert,
  ]

  # No dependencies on other services
}

Pattern 2: Optional Feature Dependencies

If a service has optional integrations:

locals {
  # Dynamic configuration based on what's enabled
  my_service_config = merge(
    {
      # Base configuration
      base_setting = "value"
    },
    var.feature_x_enabled ? {
      # Optional feature X config
      feature_x_setting = "value"
    } : {}
  )
}

resource "kubernetes_stateful_set" "my_service" {
  count = var.my_service_enabled ? 1 : 0

  depends_on = [
    # Optional dependencies handled by Terraform's count logic
    kubernetes_service.feature_x,  # No [0] - works if count = 0
  ]
}

Pattern 3: Per-Resource Processing with for_each

Instead of batch processing all services:

# ❌ Old: Batch processing with static dependencies
resource "null_resource" "update_all_dns" {
  depends_on = [
    kubernetes_stateful_set.service_a,
    kubernetes_stateful_set.service_b,
    kubernetes_stateful_set.service_c,
  ]

  provisioner "local-exec" {
    command = "python update_dns.py --all"
  }
}

# βœ… New: Per-service processing
locals {
  enabled_services = {
    for svc in local.all_services :
    svc.name => svc
    if svc.enabled
  }
}

resource "null_resource" "update_dns_per_service" {
  for_each = local.enabled_services

  # Only runs for enabled services
  # No hardcoded dependencies

  provisioner "local-exec" {
    command = "python update_dns.py --service ${each.key}"
  }
}

πŸ” Validation Checklist

Before adding depends_on, ask:

  1. Does the dependency use count or for_each?

    • If yes, do NOT use [0] or [key] indexing
    • Reference the resource directly
  2. Is the dependency always available?

    • If no, make sure the script/config handles missing resources
  3. Can I use retry logic instead?

    • Prefer waiting for actual readiness over static dependencies
  4. Is this a cross-service dependency?

    • Consider if services should be decoupled
    • Document why the dependency is necessary
  5. Will this break if services are enabled/disabled?

    • Test with different combinations of enabled services

πŸ› οΈ Migration Guide

Fixing Existing [0] References

Find all problematic references:

grep -r "\[0\]" *.tf | grep "depends_on" -B3

Replace pattern:

# Before
depends_on = [resource.name[0]]

# After
depends_on = [resource.name]

Testing Dependency Changes

  1. Test with all services enabled
  2. Test with each service disabled individually
  3. Test with random combinations
  4. Check terraform plan for errors

πŸ“š Examples from This Project

Good: Open-WebUI FreeIPA Dependency

resource "kubernetes_stateful_set" "open_webui" {
  count = var.open_webui_enabled ? 1 : 0

  depends_on = [
    # No [0] - works even if freeipa_enabled = false
    kubernetes_secret.freeipa_ca,
    kubernetes_stateful_set.freeipa,
  ]
}

Good: Per-Service DNS Updates

resource "null_resource" "dns_update_per_service" {
  for_each = local.enabled_dns_services

  # Retry loop waits for actual VPN IP
  provisioner "local-exec" {
    command = <<-EOT
      for i in {1..30}; do
        if python update_dns.py --service ${each.key}; then
          exit 0
        fi
        sleep 10
      done
    EOT
  }
}

Good: Cleanup Without Static Dependencies

resource "null_resource" "post_deployment_cleanup" {
  count = var.headscale_enabled ? 1 : 0

  # No StatefulSet dependencies - script handles missing services
  provisioner "local-exec" {
    command = "python cleanup.py"
  }
}

🚨 Anti-Patterns to Avoid

❌ Don't: Hardcode All Services

resource "null_resource" "something" {
  depends_on = [
    kubernetes_stateful_set.service_a[0],
    kubernetes_stateful_set.service_b[0],
    kubernetes_stateful_set.service_c[0],
    kubernetes_stateful_set.service_d[0],
  ]
}

❌ Don't: Use [0] in depends_on

depends_on = [null_resource.something[0]]

❌ Don't: Create Circular Dependencies

# Service A depends on B
# Service B depends on A
# Terraform will detect and fail

πŸ“– Additional Resources