terraform-aws-modules/terraform-aws-alb

Multiple conditions in load balancer rules throw the error "Only one of host_header, http_header, http_request_method, path_pattern, query_string or source_ip can be set in a condition block"

cdhesse opened this issue · 9 comments

Description

https://github.com/terraform-aws-modules/terraform-aws-alb/blob/7ed6f047d37510f60c866efc984757fc6b660007/main.tf#L343C3-L343C11

Currently, multiple conditions, such as path_pattern and host_header together, generate a single condition block, which gives the error
"Only one of host_header, http_header, http_request_method, path_pattern, query_string or source_ip can be set in a condition block"

This should look like this - taken from the terraform aws provider documentation: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener_rule

resource "aws_lb_listener_rule" "static" {
  listener_arn = aws_lb_listener.front_end.arn
  priority     = 100

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.static.arn
  }

  condition {
    path_pattern {
      values = ["/static/*"]
    }
  }

  condition {
    host_header {
      values = ["example.com"]
    }
  }
}
  • [ x ] ✋ I have searched the open/closed issues and my issue is not listed.

⚠️ Note

Before you submit an issue, please perform the following first:

  1. Remove the local .terraform directory (! ONLY if state is stored remotely, which hopefully you are following that best practice!): rm -rf .terraform/
  2. Re-initialize the project root to pull down modules: terraform init
  3. Re-attempt your terraform plan or apply and check if the issue still persists

Versions

  • Module version [Required]: 9.2.0
  • Terraform version: 1.5.5
  • Provider version(s): 5.26.0

Reproduction Code [Required]

Steps to reproduce the behavior:

No Yes terraform plan, terraform apply

Expected behavior

Multiple conditions in listener rules should work as expected

Actual behavior

Multiple conditions render incorrectly as a single condition block

Terminal Output Screenshot(s)

Additional context

do you have a reproduction?

Just modify your example ex-cognito rule to include a second condition, as below.

module "alb" {
  source = "../../"

  name    = local.name
  vpc_id  = module.vpc.vpc_id
  subnets = module.vpc.public_subnets

  # For example only
  enable_deletion_protection = false

  # Security Group
  security_group_ingress_rules = {
    all_http = {
      from_port   = 80
      to_port     = 82
      ip_protocol = "tcp"
      description = "HTTP web traffic"
      cidr_ipv4   = "0.0.0.0/0"
    }
    all_https = {
      from_port   = 443
      to_port     = 445
      ip_protocol = "tcp"
      description = "HTTPS web traffic"
      cidr_ipv4   = "0.0.0.0/0"
    }
  }
  security_group_egress_rules = {
    all = {
      ip_protocol = "-1"
      cidr_ipv4   = module.vpc.vpc_cidr_block
    }
  }

  access_logs = {
    bucket = module.log_bucket.s3_bucket_id
  }

  listeners = {
    ex-http-https-redirect = {
      port     = 80
      protocol = "HTTP"
      redirect = {
        port        = "443"
        protocol    = "HTTPS"
        status_code = "HTTP_301"
      }

      rules = {
        ex-fixed-response = {
          priority = 3
          actions = [{
            type         = "fixed-response"
            content_type = "text/plain"
            status_code  = 200
            message_body = "This is a fixed response"
          }]

          conditions = [{
            http_header = {
              http_header_name = "x-Gimme-Fixed-Response"
              values           = ["yes", "please", "right now"]
            }
          }]
        }

        ex-weighted-forward = {
          priority = 4
          actions = [{
            type = "weighted-forward"
            target_groups = [
              {
                target_group_key = "ex-lambda-with-trigger"
                weight           = 2
              },
              {
                target_group_key = "ex-instance"
                weight           = 1
              }
            ]
            stickiness = {
              enabled  = true
              duration = 3600
            }
          }]

          conditions = [{
            query_string = {
              key   = "weighted"
              value = "true"
            }
          }]
        }

        ex-redirect = {
          priority = 5000
          actions = [{
            type        = "redirect"
            status_code = "HTTP_302"
            host        = "www.youtube.com"
            path        = "/watch"
            query       = "v=dQw4w9WgXcQ"
            protocol    = "HTTPS"
          }]

          conditions = [{
            query_string = {
              key   = "video"
              value = "random"
            }
          }]
        }
      }
    }

    ex-http-weighted-target = {
      port     = 81
      protocol = "HTTP"
      weighted_forward = {
        target_groups = [
          {
            target_group_key = "ex-lambda-with-trigger"
            weight           = 60
          },
          {
            target_group_key = "ex-instance"
            weight           = 40
          }
        ]
      }
    }

    ex-fixed-response = {
      port     = 82
      protocol = "HTTP"
      fixed_response = {
        content_type = "text/plain"
        message_body = "Fixed message"
        status_code  = "200"
      }
    }

    ex-https = {
      port                        = 443
      protocol                    = "HTTPS"
      ssl_policy                  = "ELBSecurityPolicy-TLS13-1-2-Res-2021-06"
      certificate_arn             = module.acm.acm_certificate_arn
      additional_certificate_arns = [module.wildcard_cert.acm_certificate_arn]

      forward = {
        target_group_key = "ex-instance"
      }

      rules = {
        ex-cognito = {
          actions = [
            {
              type                       = "authenticate-cognito"
              on_unauthenticated_request = "authenticate"
              session_cookie_name        = "session-${local.name}"
              session_timeout            = 3600
              user_pool_arn              = aws_cognito_user_pool.this.arn
              user_pool_client_id        = aws_cognito_user_pool_client.this.id
              user_pool_domain           = aws_cognito_user_pool_domain.this.domain
            },
            {
              type             = "forward"
              target_group_key = "ex-instance"
            }
          ]

          conditions = [{
             host_header = {
              values = ["foobar.com"]
            }
            path_pattern = {
              values = ["/some/auth/required/route"]
            }
          }]
        }

        ex-fixed-response = {
          priority = 3
          actions = [{
            type         = "fixed-response"
            content_type = "text/plain"
            status_code  = 200
            message_body = "This is a fixed response"
          }]

          conditions = [{
            http_header = {
              http_header_name = "x-Gimme-Fixed-Response"
              values           = ["yes", "please", "right now"]
            }
          }]
        }

        ex-weighted-forward = {
          priority = 4
          actions = [{
            type = "weighted-forward"
            target_groups = [
              {
                target_group_key = "ex-instance"
                weight           = 2
              },
              {
                target_group_key = "ex-lambda-with-trigger"
                weight           = 1
              }
            ]
            stickiness = {
              enabled  = true
              duration = 3600
            }
          }]

          conditions = [{
            query_string = {
              key   = "weighted"
              value = "true"
            }
          }]
        }

        ex-redirect = {
          priority = 5000
          actions = [{
            type        = "redirect"
            status_code = "HTTP_302"
            host        = "www.youtube.com"
            path        = "/watch"
            query       = "v=dQw4w9WgXcQ"
            protocol    = "HTTPS"
          }]

          conditions = [{
            query_string = {
              key   = "video"
              value = "random"
            }
          }]
        }
      }
    }

    ex-cognito = {
      port            = 444
      protocol        = "HTTPS"
      certificate_arn = module.acm.acm_certificate_arn

      authenticate_cognito = {
        authentication_request_extra_params = {
          display = "page"
          prompt  = "login"
        }
        on_unauthenticated_request = "authenticate"
        session_cookie_name        = "session-${local.name}"
        session_timeout            = 3600
        user_pool_arn              = aws_cognito_user_pool.this.arn
        user_pool_client_id        = aws_cognito_user_pool_client.this.id
        user_pool_domain           = aws_cognito_user_pool_domain.this.domain
      }

      forward = {
        target_group_key = "ex-instance"
      }

      rules = {
        ex-oidc = {
          priority = 2

          actions = [
            {
              type = "authenticate-oidc"
              authentication_request_extra_params = {
                display = "page"
                prompt  = "login"
              }
              authorization_endpoint = "https://${var.domain_name}/auth"
              client_id              = "client_id"
              client_secret          = "client_secret"
              issuer                 = "https://${var.domain_name}"
              token_endpoint         = "https://${var.domain_name}/token"
              user_info_endpoint     = "https://${var.domain_name}/user_info"
            },
            {
              type             = "forward"
              target_group_key = "ex-lambda-with-trigger"
            }
          ]

          conditions = [{
            host_header = {
              values = ["foobar.com"]
            }
          }]
        }
      }
    }

    ex-oidc = {
      port            = 445
      protocol        = "HTTPS"
      certificate_arn = module.acm.acm_certificate_arn
      action_type     = "authenticate-oidc"
      authenticate_oidc = {
        authentication_request_extra_params = {
          display = "page"
          prompt  = "login"
        }
        authorization_endpoint = "https://${var.domain_name}/auth"
        client_id              = "client_id"
        client_secret          = "client_secret"
        issuer                 = "https://${var.domain_name}"
        token_endpoint         = "https://${var.domain_name}/token"
        user_info_endpoint     = "https://${var.domain_name}/user_info"
      }

      forward = {
        target_group_key = "ex-instance"
      }
    }
  }

  target_groups = {
    ex-instance = {
      name_prefix                       = "h1"
      protocol                          = "HTTP"
      port                              = 80
      target_type                       = "instance"
      deregistration_delay              = 10
      load_balancing_cross_zone_enabled = false

      health_check = {
        enabled             = true
        interval            = 30
        path                = "/healthz"
        port                = "traffic-port"
        healthy_threshold   = 3
        unhealthy_threshold = 3
        timeout             = 6
        protocol            = "HTTP"
        matcher             = "200-399"
      }

      protocol_version = "HTTP1"
      target_id        = aws_instance.this.id
      port             = 80
      tags = {
        InstanceTargetGroupTag = "baz"
      }
    }

    ex-lambda-with-trigger = {
      name_prefix                        = "l1-"
      target_type                        = "lambda"
      lambda_multi_value_headers_enabled = true
      target_id                          = module.lambda_with_allowed_triggers.lambda_function_arn
    }

    ex-lambda-without-trigger = {
      name_prefix              = "l2-"
      target_type              = "lambda"
      target_id                = module.lambda_without_allowed_triggers.lambda_function_arn
      attach_lambda_permission = true
    }
  }

  # Route53 Record(s)
  route53_records = {
    A = {
      name    = local.name
      type    = "A"
      zone_id = data.aws_route53_zone.this.id
    }
    AAAA = {
      name    = local.name
      type    = "AAAA"
      zone_id = data.aws_route53_zone.this.id
    }
  }

  tags = local.tags
}

I have not had time to fork and test, but it looks to be simple fix:

dynamic "condition" {
    for_each = try(each.value.conditions, [])

    content {
    ...

Needs to be repeated for each type instead of one big for_each. Not sure if you prefer to do an "if" in the for to try for each sub key, or just let it be []

do you have a minimum reproducible example? Lets focus on just the bare minimum example and work from there

I can delete things from your example if you like...

provider "aws" {
  region = local.region
}

data "aws_availability_zones" "available" {}

locals {
  region = "eu-west-1"
  name   = "ex-${basename(path.cwd)}"

  vpc_cidr = "10.0.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)

  tags = {
    Name       = local.name
    Example    = local.name
    Repository = "https://github.com/terraform-aws-modules/terraform-aws-alb"
  }
}

##################################################################
# Application Load Balancer
##################################################################

module "alb" {
  source = "../../"

  name    = local.name
  vpc_id  = module.vpc.vpc_id
  subnets = module.vpc.public_subnets

  # For example only
  enable_deletion_protection = false

  # Security Group
  security_group_ingress_rules = {
    all_https = {
      from_port   = 443
      to_port     = 445
      ip_protocol = "tcp"
      description = "HTTPS web traffic"
      cidr_ipv4   = "0.0.0.0/0"
    }
  }
  security_group_egress_rules = {
    all = {
      ip_protocol = "-1"
      cidr_ipv4   = module.vpc.vpc_cidr_block
    }
  }

  listeners = {
    ex-https = {
      port                        = 443
      protocol                    = "HTTPS"
      ssl_policy                  = "ELBSecurityPolicy-TLS13-1-2-Res-2021-06"
      certificate_arn             = module.acm.acm_certificate_arn
      additional_certificate_arns = [module.wildcard_cert.acm_certificate_arn]

      forward = {
        target_group_key = "ex-instance"
      }

      rules = {

        ex-fixed-response = {
          priority = 3
          actions = [{
            type         = "fixed-response"
            content_type = "text/plain"
            status_code  = 200
            message_body = "This is a fixed response"
          }]
          conditions = [{
            host_header = {
              values = ["foobar.com"]
            }
            http_header = {
              http_header_name = "x-Gimme-Fixed-Response"
              values           = ["yes", "please", "right now"]
            }
            
          }]
        }
      }
    }
  }
  target_groups = {
    ex-instance = {
      name_prefix                       = "h1"
      protocol                          = "HTTP"
      port                              = 80
      target_type                       = "instance"
      deregistration_delay              = 10
      load_balancing_cross_zone_enabled = false

      health_check = {
        enabled             = true
        interval            = 30
        path                = "/healthz"
        port                = "traffic-port"
        healthy_threshold   = 3
        unhealthy_threshold = 3
        timeout             = 6
        protocol            = "HTTP"
        matcher             = "200-399"
      }

      protocol_version = "HTTP1"
      target_id        = aws_instance.this.id
      port             = 80
      tags = {
        InstanceTargetGroupTag = "baz"
      }
    }

  tags = local.tags
}

################################################################################
# Supporting resources
################################################################################

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = local.name
  cidr = local.vpc_cidr

  azs             = local.azs
  private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)]
  public_subnets  = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)]

  tags = local.tags
}

data "aws_route53_zone" "this" {
  name = var.domain_name
}

module "acm" {
  source  = "terraform-aws-modules/acm/aws"
  version = "~> 4.0"

  domain_name = var.domain_name
  zone_id     = data.aws_route53_zone.this.id
}

module "wildcard_cert" {
  source  = "terraform-aws-modules/acm/aws"
  version = "~> 4.0"

  domain_name = "*.${var.domain_name}"
  zone_id     = data.aws_route53_zone.this.id
}

data "aws_ssm_parameter" "al2" {
  name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}

resource "aws_instance" "this" {
  ami           = data.aws_ssm_parameter.al2.value
  instance_type = "t3.nano"
  subnet_id     = element(module.vpc.private_subnets, 0)
}

I don't need the example that we provide, I just need a minimal reproducible example and a detailed explanation of what is trying to be accomplished, steps to reproduce, and the current result vs the expected result. Jumping straight into the code or suggested fixes tend to lead to these long back and forths because the proper details weren't provided from the beginning so that we can properly triage and troubleshoot

I don't need the example that we provide, I just need a minimal reproducible example and a detailed explanation of what is trying to be accomplished, steps to reproduce, and the current result vs the expected result. Jumping straight into the code or suggested fixes tend to lead to these long back and forths because the proper details weren't provided from the beginning so that we can properly triage and troubleshoot

How is what i provided not meeting this request? If you use the example I provided, do terraform plan, terraform apply, you will see the error I indicated in the bug report.

What I'm trying to accomplish:
create a listener rule with multiple conditions

If you want JUST the code snippet, it's the below from the example... it's "any multiple condition in any rule"...

  rules = {
        ex-fixed-response = {
          priority = 3
          actions = [{
            type         = "fixed-response"
            content_type = "text/plain"
            status_code  = 200
            message_body = "This is a fixed response"
          }]
          conditions = [{
            host_header = {
              values = ["foobar.com"]
            }
            http_header = {
              http_header_name = "x-Gimme-Fixed-Response"
              values           = ["yes", "please", "right now"]
            }
          }]
        }
      }

Now we're cookin with Crisco!

Your syntax is incorrect - you are passing a single map to conditions but should be passing multiple maps like:

      rules = {
        ex-fixed-response = {
          priority = 3
          actions = [{
            type         = "fixed-response"
            content_type = "text/plain"
            status_code  = 200
            message_body = "This is a fixed response"
          }]

          conditions = [
            {
              host_header = {
                values = ["foobar.com"]
              }
            }, 
            {
              http_header = {
                http_header_name = "x-Gimme-Fixed-Response"
                values           = ["yes", "please", "right now"]
              }
            }
          ]
        }

Here is the result from the console after applying:
image

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.