{"id":149,"date":"2025-12-27T12:00:00","date_gmt":"2025-12-27T12:00:00","guid":{"rendered":"https:\/\/rezachegini.com\/?p=149"},"modified":"2026-10-30T06:56:09","modified_gmt":"2026-10-30T06:56:09","slug":"advanced-load-balancing-on-aws-http-headers-query-strings-and-network-load-balancer-with-auto-scaling","status":"publish","type":"post","link":"https:\/\/rezachegini.com\/?p=149","title":{"rendered":"Advanced Load Balancing on AWS: HTTP Headers, Query Strings, and Network Load Balancer with Auto Scaling"},"content":{"rendered":"<h3 class=\"wp-block-heading\">Why I Built These Projects<\/h3><p class=\"wp-block-paragraph\">After basic ALB routing, I wanted to explore the really advanced stuff the kind of routing that powers modern SaaS applications and high-performance systems. I built two projects:<\/p><ol class=\"wp-block-list\"><li>Advanced ALB Routing &#8211; HTTP headers, query string redirects, and complex routing logic<\/li><li>Network Load Balancer with Auto Scaling &#8211; Layer 4 load balancing with dynamic instance scaling<\/li><\/ol><p class=\"wp-block-paragraph\">These projects taught me how production systems handle complex traffic patterns and scale automatically under load.<\/p><h3 class=\"wp-block-heading\">Project 1: Advanced ALB Routing with HTTP Headers<\/h3><h3 class=\"wp-block-heading\">What Makes This Advanced?<\/h3><p class=\"wp-block-paragraph\">Basic ALB routes by path (\/app1) or host (app1.domain.com). Advanced ALB can route based on:<\/p><ul class=\"wp-block-list\"><li>HTTP headers &#8211; Custom headers in requests<\/li><li>Query strings &#8211; URL parameters like ?website=aws-eks<\/li><li>HTTP header redirects &#8211; Redirect specific domains to external URLs<\/li><li>Multiple conditions &#8211; Combine different routing rules<\/li><\/ul><h3 class=\"wp-block-heading\">Real-World Use Cases<\/h3><p class=\"wp-block-paragraph\">HTTP Header Routing:<\/p><p class=\"wp-block-paragraph\">Mobile app sends: Header: &#8220;User-Agent: MyApp\/iOS\/1.0&#8221;<\/p><p class=\"wp-block-paragraph\">\u2192 Routes to mobile-optimized backend<\/p><p class=\"wp-block-paragraph\">API client sends: Header: &#8220;API-Version: v2&#8221;<\/p><p class=\"wp-block-paragraph\">\u2192 Routes to v2 API servers<\/p><p class=\"wp-block-paragraph\">Query String Redirects:<\/p><p class=\"wp-block-paragraph\">Old marketing URL: example.com?campaign=summer-sale<\/p><p class=\"wp-block-paragraph\">\u2192 Redirects to: marketing.example.com\/summer-sale<\/p><p class=\"wp-block-paragraph\">Documentation link: docs.com?topic=kubernetes<\/p><p class=\"wp-block-paragraph\">\u2192 Redirects to: docs.com\/kubernetes\/<\/p><h3 class=\"wp-block-heading\">Technical Implementation &#8211; Advanced ALB<\/h3><h3 class=\"wp-block-heading\">1. HTTP Header-Based Routing<\/h3><p class=\"wp-block-paragraph\">This is my actual configuration for routing based on custom HTTP headers:<\/p><pre class=\"wp-block-code\"><code>rules = {\n  # Rule 1: Route App1 based on custom header\n  myapp1-rule = {\n    actions = [{\n      type = \"weighted-forward\"\n      target_groups = [\n        {\n          target_group_key = \"mytg1\"\n          weight           = 1\n        }\n      ]\n      stickiness = {\n        enabled  = true\n        duration = 3600\n      }\n    }]\n    conditions = [{\n      http_header = {\n        http_header_name = \"custom-header\"\n        values = [\"app-1\", \"app1\", \"my-app-1\"]\n      }\n    }]\n  }<\/code><\/pre><p class=\"wp-block-paragraph\">How HTTP header routing works:<\/p><p class=\"wp-block-paragraph\">Request with header:<\/p><pre class=\"wp-block-code\"><code>GET \/ HTTP\/1.1\nHost: myapps11.rezaops.com\ncustom-header: app-1<\/code><\/pre><p class=\"wp-block-paragraph\">ALB checks:<\/p><ol class=\"wp-block-list\"><li>Does request have custom-header? YES<\/li><li>Is value app-1, app1, or my-app-1? YES<\/li><li>Route to mytg1 (App1 target group)<\/li><\/ol><p class=\"wp-block-paragraph\">Request without header or different value:<\/p><pre class=\"wp-block-code\"><code>GET \/ HTTP\/1.1\nHost: myapps11.rezaops.com<\/code><\/pre><p class=\"wp-block-paragraph\">\u2192 Falls through to default action (fixed response)<\/p><p class=\"wp-block-paragraph\">Multiple header values:<\/p><pre class=\"wp-block-code\"><code>values = [\"app-1\", \"app1\", \"my-app-1\"]<\/code><\/pre><p class=\"wp-block-paragraph\">ALB accepts ANY of these values. This is useful for:<\/p><ul class=\"wp-block-list\"><li>Different naming conventions: app-1 vs app1<\/li><li>Legacy compatibility: old clients use my-app-1, new use app1<\/li><li>API versioning: v1, version-1, api-v1<\/li><\/ul><p class=\"wp-block-paragraph\">Testing HTTP header routing:<\/p><pre class=\"wp-block-code\"><code># Route to App1\ncurl -H \"custom-header: app-1\" https:\/\/myapps11.rezaops.com\n\n# Route to App2\ncurl -H \"custom-header: app-2\" https:\/\/myapps11.rezaops.com\n\n# No header - fixed response\ncurl https:\/\/myapps11.rezaops.com<\/code><\/pre><h3 class=\"wp-block-heading\">2. Query String Redirect Rule<\/h3><p class=\"wp-block-paragraph\">Redirect based on URL query parameters:<\/p><pre class=\"wp-block-code\"><code>my-redirect-query = {\n  priority = 3\n  actions = [{\n    type        = \"redirect\"\n    status_code = \"HTTP_302\"\n    host        = \"stacksimplify.com\"\n    path        = \"\/aws-eks\/\"\n    query       = \"\"  # Clear original query string\n    protocol    = \"HTTPS\"\n  }]\n\n  conditions = [{\n    query_string = {\n      key   = \"website\"\n      value = \"aws-eks\"\n    }\n  }]\n}<\/code><\/pre><p class=\"wp-block-paragraph\">What happens step by step:<\/p><p class=\"wp-block-paragraph\">User visits: https:\/\/myapps11.rezaops.com?website=aws-eks<\/p><ol class=\"wp-block-list\"><li>ALB receives request<\/li><li>Checks query string: website=aws-eks<\/li><li>Matches condition \u2713<\/li><li>Returns HTTP 302 redirect:<\/li><\/ol><pre class=\"wp-block-code\"><code>HTTP\/1.1 302 Found\nLocation: https:\/\/stacksimplify.com\/aws-eks\/<\/code><\/pre><ol class=\"wp-block-list\"><li>Browser automatically follows redirect<\/li><li>User ends up at external website<\/li><\/ol><p class=\"wp-block-paragraph\">Why HTTP 302 instead of 301?<\/p><p class=\"wp-block-paragraph\">302 = Temporary Redirect<\/p><ul class=\"wp-block-list\"><li>Browser doesn&#8217;t cache<\/li><li>Can change redirect destination later<\/li><li>Analytics track each redirect<\/li><li>Used for: campaigns, tracking, temporary moves<\/li><\/ul><p class=\"wp-block-paragraph\">301 = Permanent Redirect<\/p><ul class=\"wp-block-list\"><li>Browser caches forever<\/li><li>SEO passes page rank<\/li><li>Can&#8217;t easily change later<\/li><li>Used for: permanent URL changes<\/li><\/ul><p class=\"wp-block-paragraph\">Clearing query string:<\/p><pre class=\"wp-block-code\"><code>query = \"\"<\/code><\/pre><p class=\"wp-block-paragraph\">Original URL: myapps11.rezaops.com?website=aws-eks&amp;utm=source Redirect to: stacksimplify.com\/aws-eks\/ (no query string)<\/p><p class=\"wp-block-paragraph\">To keep query string:<\/p><pre class=\"wp-block-code\"><code>query = \"#{query_string}\"  # Pass through original<\/code><\/pre><h3 class=\"wp-block-heading\">3. Host Header Redirect Rule<\/h3><p class=\"wp-block-paragraph\">Redirect entire domains to external URLs:<\/p><pre class=\"wp-block-code\"><code>my-redirect-hh = {\n  priority = 4\n  actions = [{\n    type        = \"redirect\"\n    status_code = \"HTTP_302\"\n    host        = \"stacksimplify.com\"\n    path        = \"\/azure-aks\/azure-kubernetes-service-introduction\/\"\n    query       = \"\"\n    protocol    = \"HTTPS\"\n  }]\n\n  conditions = [{\n    host_header = {\n      values = [\"azure-aks11.rezaops.com\"]\n    }\n  }]\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Use case: Domain migration<\/p><p class=\"wp-block-paragraph\">Old domain: azure-aks11.rezaops.com New domain: stacksimplify.com\/azure-aks\/&#8230;<\/p><p class=\"wp-block-paragraph\">Any request to old domain redirects to new location:<\/p><p class=\"wp-block-paragraph\">https:\/\/azure-aks11.rezaops.com<\/p><p class=\"wp-block-paragraph\">\u2192 https:\/\/stacksimplify.com\/azure-aks\/azure-kubernetes-service-introduction\/<\/p><p class=\"wp-block-paragraph\">https:\/\/azure-aks11.rezaops.com\/docs<\/p><p class=\"wp-block-paragraph\">\u2192 https:\/\/stacksimplify.com\/azure-aks\/azure-kubernetes-service-introduction\/<\/p><p class=\"wp-block-paragraph\">https:\/\/azure-aks11.rezaops.com\/anything<\/p><p class=\"wp-block-paragraph\">\u2192 https:\/\/stacksimplify.com\/azure-aks\/azure-kubernetes-service-introduction\/<\/p><p class=\"wp-block-paragraph\">Notice: ALL paths redirect to same destination. To preserve paths:<\/p><pre class=\"wp-block-code\"><code>path = \"\/azure-aks#{path}\"  # Appends original path<\/code><\/pre><h3 class=\"wp-block-heading\">4. Rule Priority and Evaluation Order<\/h3><pre class=\"wp-block-code\"><code>myapp1-rule = { }      # Priority 1 (automatic)\nmyapp2-rule = { }      # Priority 2 (automatic)\nmy-redirect-query = {\n  priority = 3         # Explicit priority\n}\nmy-redirect-hh = {\n  priority = 4\n}<\/code><\/pre><p class=\"wp-block-paragraph\">How ALB evaluates rules:<\/p><ol class=\"wp-block-list\"><li>Starts with priority 1<\/li><li>Checks conditions<\/li><li>If match \u2192 Execute action, STOP<\/li><li>If no match \u2192 Try priority 2<\/li><li>Continue until match or reach default action<\/li><\/ol><p class=\"wp-block-paragraph\">Example request flow:<\/p><p class=\"wp-block-paragraph\">Request: GET \/?website=aws-eks HTTP\/1.1<\/p><ul class=\"wp-block-list\"><li>Priority 1 (HTTP header): No custom-header \u2192 SKIP<\/li><li>Priority 2 (HTTP header): No custom-header \u2192 SKIP<\/li><li>Priority 3 (Query string): Has website=aws-eks \u2192 MATCH! Redirect to stacksimplify.com<\/li><\/ul><p class=\"wp-block-paragraph\">Request: GET \/ HTTP\/1.1 with custom-header: app-1<\/p><ul class=\"wp-block-list\"><li>Priority 1 (HTTP header): Has custom-header: app-1 \u2192 MATCH! Forward to App1<\/li><li>(Rules 2, 3, 4 never evaluated)<\/li><\/ul><h3 class=\"wp-block-heading\">5. DNS Configuration for Multiple Domains<\/h3><p class=\"wp-block-paragraph\">I create Route53 records for different routing scenarios:<\/p><pre class=\"wp-block-code\"><code># Main domain for header-based routing\nresource \"aws_route53_record\" \"default_dns\" {\n  zone_id = data.aws_route53_zone.mydomain.zone_id\n  name = \"myapps11.rezaops.com\"\n  type = \"A\"\n  \n  alias {\n    name = module.alb.dns_name\n    zone_id = module.alb.zone_id\n    evaluate_target_health = true\n  }\n}\n\n# Domain for host header redirect\nresource \"aws_route53_record\" \"app1_dns\" {\n  zone_id = data.aws_route53_zone.mydomain.zone_id\n  name = \"azure-aks11.rezaops.com\"\n  type = \"A\"\n  \n  alias {\n    name = module.alb.dns_name\n    zone_id = module.alb.zone_id\n    evaluate_target_health = true\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Both domains point to SAME ALB! The ALB decides what to do based on Host header:<\/p><p class=\"wp-block-paragraph\">myapps11.rezaops.com \u2192 Check for custom headers or query strings<\/p><p class=\"wp-block-paragraph\">azure-aks11.rezaops.com \u2192 Redirect to stacksimplify.com<\/p><h3 class=\"wp-block-heading\">Project 2: Network Load Balancer with Auto Scaling<\/h3><h3 class=\"wp-block-heading\">NLB vs ALB &#8211; When to Use Each<\/h3><p class=\"wp-block-paragraph\">Application Load Balancer (Layer 7):<\/p><ul class=\"wp-block-list\"><li>HTTP\/HTTPS only<\/li><li>Content-based routing (paths, headers, cookies)<\/li><li>SSL termination<\/li><li>WebSocket support<\/li><li>Slower (parses HTTP)<\/li><\/ul><p class=\"wp-block-paragraph\">Network Load Balancer (Layer 4):<\/p><ul class=\"wp-block-list\"><li>TCP\/UDP\/TLS protocols<\/li><li>Ultra-low latency (~100 microseconds)<\/li><li>Millions of requests per second<\/li><li>Static IP addresses<\/li><li>Preserves source IP<\/li><li>No content inspection<\/li><\/ul><p class=\"wp-block-paragraph\">When I use NLB:<\/p><ul class=\"wp-block-list\"><li>Gaming servers (low latency critical)<\/li><li>IoT applications (millions of devices)<\/li><li>TCP-based protocols (non-HTTP)<\/li><li>Need static IP for whitelisting<\/li><li>Extreme performance requirements<\/li><\/ul><h3 class=\"wp-block-heading\">Technical Implementation &#8211; NLB<\/h3><h3 class=\"wp-block-heading\">1. NLB Configuration<\/h3><pre class=\"wp-block-code\"><code>module \"nlb\" {\n  source  = \"terraform-aws-modules\/alb\/aws\"\n  version = \"9.11.0\"\n\n  name = \"mynlb\"\n  load_balancer_type = \"network\"  # KEY DIFFERENCE\n  vpc_id = module.vpc.vpc_id\n  subnets = module.vpc.public_subnets\n  dns_record_client_routing_policy = \"availability_zone_affinity\"\n  security_groups = [module.loadbalancer_sg.security_group_id]\n  \n  enable_deletion_protection = false<\/code><\/pre><p class=\"wp-block-paragraph\">Key parameter: dns_record_client_routing_policy<\/p><pre class=\"wp-block-code\"><code>dns_record_client_routing_policy = \"availability_zone_affinity\"<\/code><\/pre><p class=\"wp-block-paragraph\">This means:<\/p><p class=\"wp-block-paragraph\">Client in us-east-1a \u2192 Connects to NLB node in us-east-1a<\/p><p class=\"wp-block-paragraph\">Client in us-east-1b \u2192 Connects to NLB node in us-east-1b<\/p><p class=\"wp-block-paragraph\">Benefits:<\/p><ul class=\"wp-block-list\"><li>Lower latency (same AZ = faster)<\/li><li>Lower data transfer costs (no cross-AZ charges)<\/li><li>Better performance for latency-sensitive apps<\/li><\/ul><p class=\"wp-block-paragraph\">Alternative value:<\/p><pre class=\"wp-block-code\"><code>dns_record_client_routing_policy = \"any_availability_zone\"<\/code><\/pre><p class=\"wp-block-paragraph\">Distributes traffic evenly, ignoring client location.<\/p><h3 class=\"wp-block-heading\">2. TCP and TLS Listeners<\/h3><pre class=\"wp-block-code\"><code>listeners = {\n  # Listener 1: Plain TCP\n  my-tcp = {\n    port     = 80\n    protocol = \"TCP\"\n    forward = {\n      target_group_key = \"mytg1\"\n    }\n  }\n  \n  # Listener 2: TLS (encrypted TCP)\n  my-tls = {\n    port            = 443\n    protocol        = \"TLS\"\n    certificate_arn = module.acm.acm_certificate_arn\n    forward = {\n      target_group_key = \"mytg1\"\n    }\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">TCP vs TLS listeners:<\/p><p class=\"wp-block-paragraph\">TCP (port 80):<\/p><ul class=\"wp-block-list\"><li>No encryption<\/li><li>Load balancer passes packets directly<\/li><li>Client connects directly to backend instance<\/li><li>Fastest (no decryption overhead)<\/li><\/ul><p class=\"wp-block-paragraph\">TLS (port 443):<\/p><ul class=\"wp-block-list\"><li>Load balancer terminates SSL\/TLS<\/li><li>Decrypts traffic at NLB<\/li><li>Forwards plain HTTP to backends<\/li><li>Client sees encrypted connection<\/li><li>Backend sees plain HTTP<\/li><\/ul><p class=\"wp-block-paragraph\">Traffic flow comparison:<\/p><p class=\"wp-block-paragraph\">ALB with HTTPS:<\/p><pre class=\"wp-block-code\"><code>Client --HTTPS--&gt; ALB --HTTP--&gt; Instance\n       (decrypts here)<\/code><\/pre><p class=\"wp-block-paragraph\">NLB with TLS:<\/p><pre class=\"wp-block-code\"><code>Client --TLS--&gt; NLB --TCP--&gt; Instance\n      (decrypts here)<\/code><\/pre><p class=\"wp-block-paragraph\">NLB with TCP passthrough:<\/p><pre class=\"wp-block-code\"><code>Client --TCP--&gt; NLB --TCP--&gt; Instance\n       (no encryption, pure speed)<\/code><\/pre><h3 class=\"wp-block-heading\">3. Auto Scaling Group with Launch Template<\/h3><p class=\"wp-block-paragraph\">Launch template defines what instances look like:<\/p><pre class=\"wp-block-code\"><code>resource \"aws_launch_template\" \"my_launch_template\" {\n  name = \"my_launch_template\"\n  description = \"My launch template\"\n  image_id = data.aws_ami.amzlinux2.id\n  instance_type = var.instance_type\n  \n  vpc_security_group_ids = [module.public_bastion_sg.security_group_id]\n  key_name = var.instance_keypair\n  user_data = filebase64(\"${path.module}\/app1-install.sh\")\n  \n  ebs_optimized = true\n  update_default_version = true\n\n  block_device_mappings {\n    device_name = \"\/dev\/sda1\"\n    ebs {\n      volume_size = 20\n      volume_type = \"gp2\"\n      delete_on_termination = true\n    }\n  }\n  \n  monitoring {\n    enabled = true  # Detailed CloudWatch monitoring\n  }\n  \n  tag_specifications {\n    resource_type = \"instance\"\n    tags = {\n      Name = \"myasg\"\n    }\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">ebs_optimized = true<\/p><ul class=\"wp-block-list\"><li>Dedicated bandwidth for EBS operations<\/li><li>Better disk I\/O performance<\/li><li>Recommended for production<\/li><\/ul><p class=\"wp-block-paragraph\">update_default_version = true<\/p><ul class=\"wp-block-list\"><li>New version becomes default automatically<\/li><li>ASG uses latest version for new instances<\/li><li>Old instances keep old version until replaced<\/li><\/ul><p class=\"wp-block-paragraph\">Monitoring enabled:<\/p><ul class=\"wp-block-list\"><li>1-minute metrics instead of 5-minute<\/li><li>More granular CloudWatch data<\/li><li>Costs extra ($2.10\/instance\/month)<\/li><li>Essential for auto scaling responsiveness<\/li><\/ul><h3 class=\"wp-block-heading\">4. Auto Scaling Group Configuration<\/h3><pre class=\"wp-block-code\"><code>resource \"aws_autoscaling_group\" \"my_asg\" {\n  name_prefix = \"myasg_\"\n  desired_capacity = 2\n  max_size = 5\n  min_size = 2\n  vpc_zone_identifier = module.vpc.public_subnets\n\n  target_group_arns = [module.nlb.target_groups[\"mytg1\"].arn] \n  health_check_type = \"EC2\"\n\n  launch_template {\n    id = aws_launch_template.my_launch_template.id\n    version = aws_launch_template.my_launch_template.latest_version\n  }\n  \n  instance_refresh {\n    strategy = \"Rolling\"\n    preferences {\n      min_healthy_percentage = 50\n    }\n    triggers = [ \"desired_capacity\" ]\n  }\n\n  tag {\n    key = \"Owners\"\n    value = \"Web-Team\"\n    propagate_at_launch = true\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Instance refresh &#8211; Rolling updates:<\/p><pre class=\"wp-block-code\"><code>instance_refresh {\n  strategy = \"Rolling\"\n  preferences {\n    min_healthy_percentage = 50\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">What happens when I update launch template:<\/p><p class=\"wp-block-paragraph\">Without instance refresh:<\/p><ul class=\"wp-block-list\"><li>Old instances keep running<\/li><li>New instances use new template<\/li><li>Mixed versions in ASG<\/li><\/ul><p class=\"wp-block-paragraph\">With instance refresh (rolling):<\/p><ol class=\"wp-block-list\"><li>ASG has 4 instances<\/li><li>Terminates 2 instances (50%)<\/li><li>Launches 2 new instances with new template<\/li><li>Waits for health checks<\/li><li>Terminates remaining 2 old instances<\/li><li>Launches 2 more new instances<\/li><li>All instances now use new template<\/li><\/ol><p class=\"wp-block-paragraph\">min_healthy_percentage = 50:<\/p><ul class=\"wp-block-list\"><li>Always keeps 50% instances running<\/li><li>Lower = faster updates, more risk<\/li><li>Higher = slower updates, safer<\/li><\/ul><p class=\"wp-block-paragraph\">For production:<\/p><pre class=\"wp-block-code\"><code>min_healthy_percentage = 90  # Keep 90% healthy<\/code><\/pre><p class=\"wp-block-paragraph\">health_check_type = &#8220;EC2&#8221;<\/p><p class=\"wp-block-paragraph\">Options:<\/p><ul class=\"wp-block-list\"><li>EC2 &#8211; Checks if instance is running<\/li><li>ELB &#8211; Checks load balancer health checks<\/li><\/ul><p class=\"wp-block-paragraph\">I use EC2 for simplicity. For production, use ELB for better reliability.<\/p><h3 class=\"wp-block-heading\">5. Target Tracking Scaling Policy<\/h3><p class=\"wp-block-paragraph\">This is the magic that makes auto scaling work:<\/p><pre class=\"wp-block-code\"><code>resource \"aws_autoscaling_policy\" \"avg_cpu_policy_greater_than_xx\" {\n  name = \"avg-cpu-policy-greater-than-xx\"\n  policy_type = \"TargetTrackingScaling\"\n  autoscaling_group_name = aws_autoscaling_group.my_asg.id \n  estimated_instance_warmup = 180  # 3 minutes\n  \n  target_tracking_configuration {\n    predefined_metric_specification {\n      predefined_metric_type = \"ASGAverageCPUUtilization\"\n    }\n    target_value = 50.0  # Keep CPU at 50%\n  }\n}<\/code><\/pre><p class=\"wp-block-paragraph\">How target tracking works:<\/p><p class=\"wp-block-paragraph\">Current state: 2 instances, average CPU = 30%<\/p><ul class=\"wp-block-list\"><li>Below target (50%)<\/li><li>No action needed<\/li><\/ul><p class=\"wp-block-paragraph\">Traffic increases: 2 instances, average CPU = 70%<\/p><ul class=\"wp-block-list\"><li>Above target!<\/li><li>ASG: &#8220;I need more instances to bring average down to 50%&#8221;<\/li><li>Launches 1 more instance<\/li><li>Now: 3 instances, average CPU = 47% \u2713<\/li><\/ul><p class=\"wp-block-paragraph\">Traffic increases more: 3 instances, average CPU = 65%<\/p><ul class=\"wp-block-list\"><li>Still above target<\/li><li>Launches 1 more instance<\/li><li>Now: 4 instances, average CPU = 49% \u2713<\/li><\/ul><p class=\"wp-block-paragraph\">Traffic decreases: 4 instances, average CPU = 30%<\/p><ul class=\"wp-block-list\"><li>Below target<\/li><li>ASG: &#8220;I have too many instances&#8221;<\/li><li>Terminates 1 instance<\/li><li>Now: 3 instances, average CPU = 40% \u2713<\/li><\/ul><p class=\"wp-block-paragraph\">estimated_instance_warmup = 180<\/p><p class=\"wp-block-paragraph\">When new instance launches:<\/p><ul class=\"wp-block-list\"><li>Takes ~60 seconds to boot<\/li><li>Another ~60 seconds to install Apache<\/li><li>Another ~30 seconds to get first traffic<\/li><li>Total: ~150 seconds to be &#8220;production ready&#8221;<\/li><\/ul><p class=\"wp-block-paragraph\">During warmup period:<\/p><ul class=\"wp-block-list\"><li>Instance doesn&#8217;t count toward CPU average<\/li><li>Prevents premature scaling decisions<\/li><li>Avoids scaling thrashing<\/li><\/ul><p class=\"wp-block-paragraph\">Alternative predefined metrics:<\/p><pre class=\"wp-block-code\"><code># Request count per target\npredefined_metric_type = \"ALBRequestCountPerTarget\"\ntarget_value = 1000  # 1000 requests per instance\n\n# Network bytes in\npredefined_metric_type = \"ASGAverageNetworkIn\"\ntarget_value = 10000000  # 10MB\/s\n\n# Network bytes out\npredefined_metric_type = \"ASGAverageNetworkOut\"\ntarget_value = 20000000  # 20MB\/s<\/code><\/pre><h3 class=\"wp-block-heading\">6. Scheduled Scaling Actions<\/h3><p class=\"wp-block-paragraph\">Scale based on predictable patterns:<\/p><pre class=\"wp-block-code\"><code># Scale up at 7 AM UTC\nresource \"aws_autoscaling_schedule\" \"increase_capacity_7am\" {\n  scheduled_action_name  = \"increase-capacity-7am\"\n  min_size               = 2\n  max_size               = 10\n  desired_capacity       = 8\n  start_time             = \"2030-03-30T11:00:00Z\"\n  recurrence             = \"00 09 * * *\"  # Cron: 9 AM UTC daily\n  autoscaling_group_name = aws_autoscaling_group.my_asg.id \n}\n\n# Scale down at 9 PM UTC\nresource \"aws_autoscaling_schedule\" \"decrease_capacity_5pm\" {\n  scheduled_action_name  = \"decrease-capacity-5pm\"\n  min_size               = 2\n  max_size               = 10\n  desired_capacity       = 2\n  start_time             = \"2030-03-30T21:00:00Z\"\n  recurrence             = \"00 21 * * *\"  # Cron: 9 PM UTC daily\n  autoscaling_group_name = aws_autoscaling_group.my_asg.id\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Cron syntax explained:<\/p><pre class=\"wp-block-code\"><code>00 09 * * *\n\u2502  \u2502  \u2502 \u2502 \u2502\n\u2502  \u2502  \u2502 \u2502 \u2514\u2500 Day of week (0-7, 0=Sunday)\n\u2502  \u2502  \u2502 \u2514\u2500\u2500\u2500 Month (1-12)\n\u2502  \u2502  \u2514\u2500\u2500\u2500\u2500\u2500 Day of month (1-31)\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Hour (0-23)\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Minute (0-59)<\/code><\/pre><p class=\"wp-block-paragraph\">Examples:<\/p><pre class=\"wp-block-code\"><code>00 09 * * *        # Every day at 9 AM\n00 09 * * 1-5      # Weekdays at 9 AM\n00 *\/4 * * *       # Every 4 hours\n30 08 1 * *        # First day of month at 8:30 AM\n00 18 * * 5        # Every Friday at 6 PM<\/code><\/pre><p class=\"wp-block-paragraph\">Real-world scheduling patterns:<\/p><p class=\"wp-block-paragraph\">E-commerce site:<\/p><pre class=\"wp-block-code\"><code># Black Friday preparation\nrecurrence = \"00 06 25 11 *\"  # Nov 25 at 6 AM\ndesired_capacity = 20\n\n# Post-Christmas scale down\nrecurrence = \"00 00 27 12 *\"  # Dec 27 at midnight\ndesired_capacity = 5<\/code><\/pre><p class=\"wp-block-paragraph\">Business hours scaling:<\/p><pre class=\"wp-block-code\"><code># Monday-Friday 8 AM\nrecurrence = \"00 08 * * 1-5\"\ndesired_capacity = 10\n\n# Monday-Friday 6 PM\nrecurrence = \"00 18 * * 1-5\"\ndesired_capacity = 2<\/code><\/pre><p class=\"wp-block-paragraph\">scheduled vs target tracking:<\/p><p class=\"wp-block-paragraph\">Both can run simultaneously!<\/p><ul class=\"wp-block-list\"><li>Scheduled: Sets capacity at specific times<\/li><li>Target tracking: Adjusts capacity based on metrics<\/li><\/ul><p class=\"wp-block-paragraph\">Example scenario:<\/p><p class=\"wp-block-paragraph\">7 AM: Scheduled action sets desired=8<\/p><p class=\"wp-block-paragraph\">8 AM: Traffic spike, CPU&gt;50%, target tracking adds 2 more \u2192 10 instances<\/p><p class=\"wp-block-paragraph\">9 AM: Traffic normal, CPU&lt;50%, target tracking removes 2 \u2192 8 instances<\/p><p class=\"wp-block-paragraph\">9 PM: Scheduled action sets desired=2<\/p><h3 class=\"wp-block-heading\">7. SNS Notifications for Scaling Events<\/h3><p class=\"wp-block-paragraph\">Get email alerts when ASG scales:<\/p><pre class=\"wp-block-code\"><code># Create SNS topic\nresource \"aws_sns_topic\" \"myasg_sns_topic\" {\n  name = \"myasg-sns-topic-${random_pet.this.id}\"\n}\n\n# Subscribe email\nresource \"aws_sns_topic_subscription\" \"myasg_sns_topic_subscription\" {\n  topic_arn = aws_sns_topic.myasg_sns_topic.arn\n  protocol  = \"email\"\n  endpoint  = \"rezachegini1994@gmail.com\"\n}\n\n# Configure notifications\nresource \"aws_autoscaling_notification\" \"myasg_notifications\" {\n  group_names = [aws_autoscaling_group.my_asg.id]\n  notifications = [\n    \"autoscaling:EC2_INSTANCE_LAUNCH\",\n    \"autoscaling:EC2_INSTANCE_TERMINATE\",\n    \"autoscaling:EC2_INSTANCE_LAUNCH_ERROR\",\n    \"autoscaling:EC2_INSTANCE_TERMINATE_ERROR\",\n  ]\n  topic_arn = aws_sns_topic.myasg_sns_topic.arn \n}<\/code><\/pre><p class=\"wp-block-paragraph\">Email notifications I receive:<\/p><p class=\"wp-block-paragraph\">Instance launch:<\/p><pre class=\"wp-block-code\"><code>Event: autoscaling:EC2_INSTANCE_LAUNCH\nTime: 2025-01-15 10:30:00\nGroup: myasg_20250115\nCause: Target tracking increased capacity\nInstance: i-0abcd1234efgh5678<\/code><\/pre><p class=\"wp-block-paragraph\">Instance terminate:<\/p><pre class=\"wp-block-code\"><code>Event: autoscaling:EC2_INSTANCE_TERMINATE\nTime: 2025-01-15 22:15:00\nGroup: myasg_20250115\nCause: Scheduled action decreased capacity\nInstance: i-0abcd1234efgh5678<\/code><\/pre><p class=\"wp-block-paragraph\">Launch error:<\/p><pre class=\"wp-block-code\"><code>Event: autoscaling:EC2_INSTANCE_LAUNCH_ERROR\nTime: 2025-01-15 15:45:00\nGroup: myasg_20250115\nCause: Insufficient capacity in AZ\nStatusMessage: We currently do not have sufficient t2.micro capacity<\/code><\/pre><p class=\"wp-block-paragraph\">random_pet for unique names:<\/p><pre class=\"wp-block-code\"><code>resource \"random_pet\" \"this\" {\n  length = 2\n}\n# Generates: \"mighty-panda\", \"happy-tiger\", etc.<\/code><\/pre><p class=\"wp-block-paragraph\">I use this to avoid SNS topic name conflicts when deploying multiple times.<\/p><h3 class=\"wp-block-heading\">Comparing Load Balancer Types<\/h3><h3 class=\"wp-block-heading\">Application Load Balancer (ALB)<\/h3><ul class=\"wp-block-list\"><li>OSI Layer: Layer 7 (Application)<\/li><li>Protocols: HTTP, HTTPS, WebSocket<\/li><li>Latency: ~10\u201320 ms<\/li><li>Throughput: ~100K requests\/sec<\/li><li>Routing: Path, host header, query string<\/li><li>SSL Termination: Yes<\/li><li>Static IP: No (DNS-based)<\/li><li>Client IP: Lost (forwarded via headers)<\/li><li>Health Checks: HTTP-based<\/li><li>Best for: Web apps, APIs, microservices<\/li><\/ul><h3 class=\"wp-block-heading\">Network Load Balancer (NLB)<\/h3><ul class=\"wp-block-list\"><li>OSI Layer: Layer 4 (Transport)<\/li><li>Protocols: TCP, UDP, TLS<\/li><li>Latency: ~100 microseconds<\/li><li>Throughput: Millions of requests\/sec<\/li><li>Routing: IP + Port<\/li><li>SSL Termination: Yes (TLS listeners)<\/li><li>Static IP: Yes (per AZ)<\/li><li>Client IP: Preserved<\/li><li>Health Checks: TCP-based<\/li><li>Best for: Gaming, IoT, real-time &amp; non-HTTP workloads<\/li><\/ul><h3 class=\"wp-block-heading\">Real-World Lessons<\/h3><h3 class=\"wp-block-heading\">1. HTTP Header Case Sensitivity<\/h3><p class=\"wp-block-paragraph\">My mistake:<\/p><pre class=\"wp-block-code\"><code>http_header_name = \"Custom-Header\"<\/code><\/pre><p class=\"wp-block-paragraph\">Tested with:<\/p><pre class=\"wp-block-code\"><code>curl -H \"custom-header: app-1\" https:\/\/myapps11.rezaops.com<\/code><\/pre><p class=\"wp-block-paragraph\">Result: Didn&#8217;t work!<\/p><p class=\"wp-block-paragraph\">Why: HTTP headers are case-insensitive in HTTP spec, but ALB conditions are case-sensitive!<\/p><p class=\"wp-block-paragraph\">Solution:<\/p><pre class=\"wp-block-code\"><code>http_header_name = \"custom-header\"  # Lowercase<\/code><\/pre><p class=\"wp-block-paragraph\">Or configure ALB to normalize headers (not available in Terraform module).<\/p><h3 class=\"wp-block-heading\">2. Query String Encoding<\/h3><p class=\"wp-block-paragraph\">Problem: URL with spaces: https:\/\/myapps11.rezaops.com?website=aws eks<\/p><p class=\"wp-block-paragraph\">Didn&#8217;t match:<\/p><pre class=\"wp-block-code\"><code>query_string = {\n  key = \"website\"\n  value = \"aws eks\"\n}<\/code><\/pre><p class=\"wp-block-paragraph\">Reason: Browsers URL-encode spaces: aws%20eks<\/p><p class=\"wp-block-paragraph\">Solution:<\/p><pre class=\"wp-block-code\"><code>value = \"aws%20eks\"  # URL-encoded<\/code><\/pre><p class=\"wp-block-paragraph\">Or use wildcards:<\/p><pre class=\"wp-block-code\"><code>value = \"aws*\"  # Matches \"aws eks\", \"aws-eks\", \"aws_eks\"<\/code><\/pre><h3 class=\"wp-block-heading\">3. Auto Scaling Cooldown vs Warmup<\/h3><p class=\"wp-block-paragraph\">I confused these two concepts:<\/p><p class=\"wp-block-paragraph\">Cooldown (old, don&#8217;t use):<\/p><ul class=\"wp-block-list\"><li>Wait period after scaling action<\/li><li>Blocks ALL scaling during cooldown<\/li><li>Too rigid for modern applications<\/li><\/ul><p class=\"wp-block-paragraph\">Warmup (modern, use this):<\/p><pre class=\"wp-block-code\"><code>estimated_instance_warmup = 180<\/code><\/pre><ul class=\"wp-block-list\"><li>Excludes new instances from metrics temporarily<\/li><li>Doesn&#8217;t block other scaling actions<\/li><li>Better for dynamic workloads<\/li><\/ul><p class=\"wp-block-paragraph\">Target tracking uses warmup automatically. Only set cooldown for simple scaling policies.<\/p><h3 class=\"wp-block-heading\">4. ASG Instance Refresh Failed<\/h3><p class=\"wp-block-paragraph\">Error:<\/p><pre class=\"wp-block-code\"><code>Instance refresh failed: MinHealthyPercentage requirement not met<\/code><\/pre><p class=\"wp-block-paragraph\">What happened:<\/p><ul class=\"wp-block-list\"><li>Tried to refresh 4 instances<\/li><li>min_healthy_percentage = 75<\/li><li>Killed 1 instance (3 left = 75%)<\/li><li>New instance took 5 minutes to be healthy<\/li><li>During that time, only 3 healthy (75%)<\/li><li>Tried to kill another \u2192 Would drop to 2 (50%)<\/li><li>Refused!<\/li><\/ul><p class=\"wp-block-paragraph\">Solution:<\/p><pre class=\"wp-block-code\"><code>min_healthy_percentage = 50  # More flexible<\/code><\/pre><p class=\"wp-block-paragraph\">Or increase instance count before refresh:<\/p><pre class=\"wp-block-code\"><code># Scale up first\naws autoscaling set-desired-capacity --auto-scaling-group-name myasg --desired-capacity 6\n\n# Then refresh\naws autoscaling start-instance-refresh --auto-scaling-group-name myasg<\/code><\/pre><h3 class=\"wp-block-heading\">5. NLB Preserves Source IP &#8211; Security Group Issues<\/h3><p class=\"wp-block-paragraph\">Problem: Private instance security group:<\/p><pre class=\"wp-block-code\"><code>ingress_cidr_blocks = [module.vpc.vpc_cidr_block]  # 10.0.0.0\/16<\/code><\/pre><p class=\"wp-block-paragraph\">Connections from internet failed health checks!<\/p><p class=\"wp-block-paragraph\">Why: NLB preserves source IP. Client IP is public (not from VPC).<\/p><p class=\"wp-block-paragraph\">Solution for NLB:<\/p><pre class=\"wp-block-code\"><code>ingress_cidr_blocks = [\"0.0.0.0\/0\"]  # Allow from anywhere<\/code><\/pre><p class=\"wp-block-paragraph\">Or use ALB which rewrites source IP to ALB&#8217;s IP (inside VPC).<\/p><h3 class=\"wp-block-heading\">6. SNS Email Subscription Requires Confirmation<\/h3><p class=\"wp-block-paragraph\">What happened: Deployed infrastructure, never got emails!<\/p><p class=\"wp-block-paragraph\">Why: SNS requires email confirmation:<\/p><ol class=\"wp-block-list\"><li>Terraform creates subscription<\/li><li>AWS sends confirmation email<\/li><li>Must click &#8220;Confirm subscription&#8221;<\/li><li>Then emails start working<\/li><\/ol><p class=\"wp-block-paragraph\">Can&#8217;t automate this for security.<\/p><p class=\"wp-block-paragraph\">Check spam folder for confirmation email!<\/p><h3 class=\"wp-block-heading\">7. Launch Template Version Management<\/h3><p class=\"wp-block-paragraph\">Problem: Updated launch template, but ASG used old version!<\/p><p class=\"wp-block-paragraph\">Cause:<\/p><pre class=\"wp-block-code\"><code>version = \"$Latest\"  # Wrong!<\/code><\/pre><p class=\"wp-block-paragraph\">This doesn&#8217;t auto-update. Terraform doesn&#8217;t know to trigger refresh.<\/p><p class=\"wp-block-paragraph\">Solution:<\/p><pre class=\"wp-block-code\"><code>version = aws_launch_template.my_launch_template.latest_version<\/code><\/pre><p class=\"wp-block-paragraph\">Plus:<\/p><pre class=\"wp-block-code\"><code>update_default_version = true  # In launch template<\/code><\/pre><p class=\"wp-block-paragraph\">Now when template changes, ASG knows to refresh.<\/p><h3 class=\"wp-block-heading\">Cost Comparison<\/h3><h3 class=\"wp-block-heading\">Advanced ALB Routing:<\/h3><p class=\"wp-block-paragraph\">Same as basic ALB:<\/p><ul class=\"wp-block-list\"><li>ALB: $22.50\/month<\/li><li>LCU: ~$5-10\/month<\/li><li>ACM: Free<\/li><li>Route53: $0.50\/month<\/li><li>EC2 (4 instances): $28\/month<\/li><li>Total: ~$56-61\/month<\/li><\/ul><h3 class=\"wp-block-heading\">NLB with Auto Scaling:<\/h3><ul class=\"wp-block-list\"><li>NLB: $22.50\/month<\/li><li>NLCU: ~$5-15\/month (depends on traffic)<\/li><li>EC2 (2-5 instances): $14-35\/month (dynamic)<\/li><li>CloudWatch: $2\/month (detailed monitoring)<\/li><li>SNS: $0.50\/month<\/li><li>Total: ~$44-75\/month (varies with load)<\/li><\/ul><p class=\"wp-block-paragraph\">Cost optimization for ASG: Set aggressive scale-down:<\/p><pre class=\"wp-block-code\"><code>min_size = 1  # Scale to 1 at night\nrecurrence = \"00 22 * * *\"  # 10 PM scale down\ndesired_capacity = 1<\/code><\/pre><h3 class=\"wp-block-heading\">Deployment Commands<\/h3><h3 class=\"wp-block-heading\">Advanced ALB:<\/h3><pre class=\"wp-block-code\"><code>cd ALB-HTTPHeader-QueryString-Redirects\/terraform-manifests\nterraform init\nterraform apply\n\n# Test HTTP header routing\ncurl -H \"custom-header: app-1\" https:\/\/myapps11.rezaops.com\n\n# Test query string redirect\ncurl -I \"https:\/\/myapps11.rezaops.com?website=aws-eks\"\n\n# Test host header redirect\ncurl -I \"https:\/\/azure-aks11.rezaops.com\"<\/code><\/pre><h3 class=\"wp-block-heading\">NLB with Auto Scaling:<\/h3><pre class=\"wp-block-code\"><code>cd AWS-NLB-Network-Load-Balancer\/terraform-manifests\nterraform init\nterraform apply\n\n# Confirm SNS email subscription\n# Check email and click confirmation link\n\n# Test NLB\ncurl http:\/\/nlb.rezaops.com\ncurl https:\/\/nlb.rezaops.com\n\n# Watch auto scaling\naws autoscaling describe-auto-scaling-groups \\\n  --auto-scaling-group-names myasg_xxxxx\n\n# Trigger scaling by generating load\nab -n 100000 -c 100 http:\/\/nlb.rezaops.com\/<\/code><\/pre><h3 class=\"wp-block-heading\">What I Learned<\/h3><ol class=\"wp-block-list\"><li>ALB routing is incredibly flexible &#8211; Can route on almost any HTTP attribute<\/li><li>NLB is for performance &#8211; When latency matters more than features<\/li><li>Auto Scaling is not magic &#8211; Requires careful tuning of thresholds and warmup times<\/li><li>Scheduled scaling saves money &#8211; Scale down during off-hours<\/li><li>Target tracking is powerful &#8211; Set target CPU and forget about it<\/li><li>Instance refresh is crucial &#8211; For zero-downtime deployments<\/li><li>Monitoring is essential &#8211; SNS alerts help catch issues early<\/li><\/ol>","protected":false},"excerpt":{"rendered":"<p>Why I Built These Projects After basic ALB routing, I wanted to explore the really advanced stuff the kind of routing that&hellip;<\/p>\n","protected":false},"author":2,"featured_media":151,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_rezaops_part":"3","_rezaops_linkedin_url":"https:\/\/www.linkedin.com\/pulse\/advanced-load-balancing-aws-http-headers-query-strings-reza-chegini-wv8lc\/","footnotes":""},"categories":[85,10],"tags":[90,60,8,93,87],"series":[86],"class_list":["post-149","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aws","category-devops","tag-auto-scaling","tag-aws","tag-devops","tag-load-balancer","tag-terraform","series-aws-with-terraform"],"jetpack_featured_media_url":"https:\/\/rezachegini.com\/wp-content\/uploads\/2026\/10\/advanced-load-balancing-aws-cover.png","_links":{"self":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/149","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=149"}],"version-history":[{"count":1,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/149\/revisions"}],"predecessor-version":[{"id":150,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/149\/revisions\/150"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/media\/151"}],"wp:attachment":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=149"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=149"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=149"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=149"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}