One load balancer, several ports¶
The previous page built the pieces. Real stacks repeat them: one load balancer listening on several ports, each forwarding to its own target group. Only the port changes, so the repetition belongs in a function that returns a fragment.
def web_entrypoint(
name: str,
*,
vpc_id: Expr[str],
subnets: Expr[list[Expr[str]]],
ports: Sequence[int],
allowed_cidrs: Sequence[str],
scheme: Literal["internet-facing", "internal"] = "internet-facing",
certificate_arn: Expr[str] | None = None,
redirect_http: bool = False,
health_check_path: str = "/",
tags: list[Expr[Tag]] | None = None,
) -> Resources:
"""One load balancer in front of several ports.
Logical IDs: `{name}SecurityGroup`, `{name}LoadBalancer`, and per port `p`
`{name}TargetGroup{p}` and `{name}Listener{p}`. With `redirect_http`, port 80 is opened and
answered by `{name}HttpRedirect`.
"""
if redirect_http and (certificate_arn is None or 80 in ports):
raise ValueError("redirect_http needs a certificate_arn and no forwarded port 80")
open_ports = [*ports, 80] if redirect_http else list(ports)
group, lb = f"{name}SecurityGroup", f"{name}LoadBalancer"
ingress: list[Expr[ec2.SecurityGroupIngressProperty]] = [
{"IpProtocol": "tcp", "FromPort": p, "ToPort": p, "CidrIp": cidr}
for p in open_ports
for cidr in allowed_cidrs
]
resources: Resources = {
group: ec2.security_group(
GroupDescription=f"{name} load balancer",
VpcId=vpc_id,
SecurityGroupIngress=ingress,
),
lb: load_balancer(subnets=subnets, security_groups=[ref(group)], scheme=scheme, tags=tags),
}
for p in ports:
tg = f"{name}TargetGroup{p}"
resources[tg] = target_group(vpc_id=vpc_id, port=p, health_check_path=health_check_path, tags=tags)
resources[f"{name}Listener{p}"] = listener(
load_balancer=lb, target_group=tg, port=p, certificate_arn=certificate_arn,
)
if redirect_http:
resources[f"{name}HttpRedirect"] = redirect_http_to_https(lb)
return resources
web_entrypoint(
"Web",
vpc_id=vpc_id,
subnets=private_subnets,
ports=[8080, 9090],
allowed_cidrs=["10.0.0.0/8"],
scheme="internal",
)
creates these logical IDs (the rendered YAML shows the full stack):
| Logical ID | Resource |
|---|---|
WebSecurityGroup |
the load balancer's security group |
WebLoadBalancer |
the application load balancer |
WebTargetGroup8080, WebTargetGroup9090 |
one target group per port |
WebListener8080, WebListener9090 |
one listener per port |
Choices worth noticing¶
allowed_cidrshas no default. A default of0.0.0.0/0would be convenient and would silently expose every internal load balancer someone forgot to restrict. Making the caller write it down is a cheap safeguard.- Everything the fragment creates refers only to itself.
vpc_idandsubnetscome in as arguments (literals, parameters or imports), and everyRefinside points at another resource in the fragment. The tests check exactly that, so a fragment can be moved between stacks. redirect_http=Trueopens port 80 and adds{name}HttpRedirect. It insists on a certificate, and on port 80 not already being forwarded somewhere, because the other combinations are mistakes. An error at generation time beats a surprise at deploy.- A loop, not a template. The per-port resources come from an ordinary
forloop. There is no template language to learn, and the loop body is type-checked.
Attaching the backend¶
The Auto Scaling group (or service) that receives the traffic names the target groups it registers with, and lets the load balancer's group in:
resources = cfn.combine(
web_entrypoint("Web", vpc_id=vpc_id, subnets=subnets, ports=[8080],
allowed_cidrs=["10.0.0.0/8"], scheme="internal"),
{
"AppAccess": allow_from("AppSecurityGroup", "WebSecurityGroup", 8080),
# the group's TargetGroupARNs=[ref("WebTargetGroup8080")] connects the two
},
)
cfn.combine guards against two fragments reusing a logical ID, which is
the mistake most likely once a stack is built from several of these.