Load balancer building blocks¶
An application load balancer is four small resources that always appear together: the load balancer, a target group per backend, a listener per port that connects the two, and security groups so traffic can flow. Each one has a handful of properties you set the same way every time, which is what makes them worth wrapping.
These helpers are deliberately thin. They take plain arguments and return one generated resource, so the type checker still knows exactly what you built.
from collections.abc import Sequence
from typing import Literal
from cfn_typed_sumo.types import ec2
from cfn_typed_sumo.types import elasticloadbalancingv2 as elbv2
from cfn_typed import Expr, Resources, Tag, ref
The load balancer¶
def load_balancer(
*,
subnets: Expr[list[Expr[str]]],
security_groups: list[Expr[str]],
scheme: Literal["internet-facing", "internal"] = "internet-facing",
idle_timeout: int = 60,
tags: list[Expr[Tag]] | None = None,
) -> elbv2.LoadBalancer:
"""An application load balancer. No `Name`, so CloudFormation picks a unique one."""
props: elbv2.LoadBalancerProps = {
"Type": "application",
"Scheme": scheme,
"Subnets": subnets,
"SecurityGroups": security_groups,
"LoadBalancerAttributes": [{"Key": "idle_timeout.timeout_seconds", "Value": str(idle_timeout)}],
}
if tags:
props["Tags"] = tags
return elbv2.load_balancer(**props)
- There is no
Name. Leaving it out lets CloudFormation choose a unique one, so a replacement can be created before the old load balancer is deleted. A fixed name blocks that. schemeis aLiteral."internal"or"internet-facing"autocompletes, and a typo such as"internet_facing"is an error before deploy.idle_timeoutis the one attribute almost everyone changes, so it is an argument and the rest stay at their defaults.
The target group¶
def target_group(
*,
vpc_id: Expr[str],
port: int,
protocol: Literal["HTTP", "HTTPS"] = "HTTP",
health_check_path: str = "/",
health_check_port: str = "traffic-port",
deregistration_delay: int = 30,
target_type: Literal["instance", "ip", "lambda"] = "instance",
tags: list[Expr[Tag]] | None = None,
) -> elbv2.TargetGroup:
"""Where the traffic goes. A short deregistration delay makes deploys faster."""
props: elbv2.TargetGroupProps = {
"VpcId": vpc_id,
"Port": port,
"Protocol": protocol,
"TargetType": target_type,
"HealthCheckEnabled": True,
"HealthCheckPath": health_check_path,
"HealthCheckPort": health_check_port,
"HealthyThresholdCount": 2,
"TargetGroupAttributes": [
{"Key": "deregistration_delay.timeout_seconds", "Value": str(deregistration_delay)},
],
}
if tags:
props["Tags"] = tags
return elbv2.target_group(**props)
The short deregistration_delay is a deliberate default. The AWS default of five
minutes makes every deploy wait for connections that will never finish. Raise it for
services with long-lived requests.
Listeners¶
def listener(
*,
load_balancer: str,
target_group: str,
port: int,
certificate_arn: Expr[str] | None = None,
) -> elbv2.Listener:
"""Forward a port to a target group. A certificate turns it into an HTTPS listener."""
props: elbv2.ListenerProps = {
"LoadBalancerArn": ref(load_balancer),
"Port": port,
"Protocol": "HTTPS" if certificate_arn else "HTTP",
"DefaultActions": [{"Type": "forward", "TargetGroupArn": ref(target_group)}],
}
if certificate_arn:
props["Certificates"] = [{"CertificateArn": certificate_arn}]
props["SslPolicy"] = "ELBSecurityPolicy-TLS13-1-2-2021-06"
return elbv2.listener(**props)
def redirect_http_to_https(load_balancer: str, *, port: int = 80, to_port: int = 443) -> elbv2.Listener:
"""A listener that answers with a permanent redirect instead of forwarding."""
return elbv2.listener(
LoadBalancerArn=ref(load_balancer),
Port=port,
Protocol="HTTP",
DefaultActions=[{
"Type": "redirect",
"RedirectConfig": {"Protocol": "HTTPS", "Port": str(to_port), "StatusCode": "HTTP_301"},
}],
)
- A certificate decides the protocol. Passing
certificate_arnmakes the listener HTTPS and adds a TLS policy; leaving it out makes plain HTTP. One argument, so the two can never disagree. - Listeners and target groups are named by logical ID, not passed as objects. The
helper turns them into
ref(...). That is the same trade-off as everywhere else in this book: a short call site, with the name as a string. redirect_http_to_httpsshows a different default action. The generatedListenerActiontype has a field for each kind of action, andRedirectConfigis checked like everything else.
Letting traffic through¶
def allow_from(group: str, source_group: str, port: int) -> ec2.SecurityGroupIngress:
"""Let `source_group` (the load balancer's) reach `group` (the instances') on `port`."""
return ec2.security_group_ingress(
GroupId=ref(group),
SourceSecurityGroupId=ref(source_group),
IpProtocol="tcp",
FromPort=port,
ToPort=port,
)
The instances (or tasks) behind the load balancer should accept traffic from the load balancer's security group, not from an address range. Then it stays correct when the load balancer's addresses change, and nothing else in the VPC can reach the port.