The ApplicationLoadBalancerDecorator allows you to expose lambda functions as Application Load Balancer targets.
This can be useful to provide HTTP(S) access to one or more Lambda functions without requiring an API-Gateway service.
Application Load Balancer (ALB) lambda targets must satisfy a prescribed Lambda signature:
import (
awsEvents "github.com/aws/aws-lambda-go/events"
)
func(context.Context, awsEvents.ALBTargetGroupRequest) awsEvents.ALBTargetGroupResponse
See the ALBTargetGroupRequest and ALBTargetGroupResponse godoc entries for more information.
An example ALB-eligible target function might look like:
// ALB eligible lambda function
func helloNewWorld(ctx context.Context,
albEvent awsEvents.ALBTargetGroupRequest) (awsEvents.ALBTargetGroupResponse, error) {
return awsEvents.ALBTargetGroupResponse{
StatusCode: 200,
StatusDescription: fmt.Sprintf("200 OK"),
Body: "Some other handler",
IsBase64Encoded: false,
Headers: map[string]string{},
}, nil
}
Once you’ve defined your ALB-compatible functions, the next step is to register them with the decorator responsible for configuring them as ALB listener targets.
The ApplicationLoadBalancerDecorator
satisfies the ServiceDecoratorHookHandler interface and adds a set of CloudFormation Resources to support properly publishing your Lambda functions.
Since this access path requires an Application Load Balancer, the first step is to define the SecurityGroup associated with the ALB so that incoming requests can be accepted.
The following definition will create an Security Group that accepts public traffic on port 80
:
sgResName := sparta.CloudFormationResourceName("ALBSecurityGroup", "ALBSecurityGroup")
sgRes := &gocf.EC2SecurityGroup{
GroupDescription: gocf.String("ALB Security Group"),
SecurityGroupIngress: &gocf.EC2SecurityGroupIngressPropertyList{
gocf.EC2SecurityGroupIngressProperty{
IPProtocol: gocf.String("tcp"),
FromPort: gocf.Integer(80),
ToPort: gocf.Integer(80),
CidrIP: gocf.String("0.0.0.0/0"),
},
},
}
The subnets for our Application Load Balancer are supplied as an environment variable (TEST_SUBNETS) of the form id1,id2
:
subnetList := strings.Split(os.Getenv("TEST_SUBNETS"), ",")
subnetIDs := make([]gocf.Stringable, len(subnetList))
for eachIndex, eachSubnet := range subnetList {
subnetIDs[eachIndex] = gocf.String(eachSubnet)
}
The next step is to define the ALB and associate it with both the account Subnets and SecurityGroup we just defined:
alb := &gocf.ElasticLoadBalancingV2LoadBalancer{
Subnets: gocf.StringList(subnetIDs...),
SecurityGroups: gocf.StringList(gocf.GetAtt(sgResName, "GroupId")),
}
This ElasticLoadBalancingV2LoadBalancer
instance is provided to NewApplicationLoadBalancerDecorator
to create the decorator that will annotate the CloudFormation
template with the required resources.
albDecorator, albDecoratorErr := spartaDecorators.NewApplicationLoadBalancerDecorator(alb,
80,
"HTTP",
lambdaFn)
The NewApplicationLoadBalancerDecorator
accepts four arguments:
ElasticLoadBalancingV2LoadBalancer
that handles this service’s incoming requests80
) that incoming requests will be acceptedHTTP
) for incoming requestslambdaFn
) to use as the ALB’s DefaultAction handler in case no other conditional target matches the incoming request.Services may expose more than one Lambda function on that same port using multiple ListenerRule entries.
For instance, to register a second lambda function lambdaFn2
with the same Application Load Balancer at the /newhello path, add a ConditionalEntry
as in:
albDecorator.AddConditionalEntry(gocf.ElasticLoadBalancingV2ListenerRuleRuleCondition{
Field: gocf.String("path-pattern"),
PathPatternConfig: &gocf.ElasticLoadBalancingV2ListenerRulePathPatternConfig{
Values: gocf.StringList(gocf.String("/newhello*")),
},
}, lambdaFn2)
This will create a rule that associates the /newhello* path with lambdaFn2
. Requests that do not match the incoming path will fallback to the default handler (lambdaFn
).
See the RuleCondition documentation
for the full set of conditions that can be expressed.
The next step is to ensure that the Security Group that we associated with our ALB is included in the final template. This is done by including it in the ApplicationLoadBalancerDecorator.Resources
map which allows you to provide additional CloudFormation resources that should be included in the final template:
// Finally, tell the ALB decorator we have some additional resources that need to be
// included in the CloudFormation template
albDecorator.Resources[sgResName] = sgRes
With the decorator fully configured, the final step is to provide it as part of the WorkflowHooks struct:
// Supply it to the WorkflowHooks and get going...
workflowHooks := &sparta.WorkflowHooks{
ServiceDecorators: []sparta.ServiceDecoratorHookHandler{
albDecorator,
},
}
err := sparta.MainEx(awsName,
"Simple Sparta application that demonstrates how to make Lambda functions an ALB Target",
lambdaFunctions,
nil,
nil,
workflowHooks,
false)
As part of the provisioning workflow, the ApplicationLoadBalancerDecorator
will include the Application Load Balancer discovery information in the Outputs section as in:
INFO[0056] Stack Outputs ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
INFO[0056] ApplicationLoadBalancerDNS80 Description="ALB DNSName (port: 80, protocol: HTTP)" Value=MyALB-ELBv2-44R3J0MV1D37-943334334.us-west-2.elb.amazonaws.com
INFO[0056] ApplicationLoadBalancerName80 Description="ALB Name (port: 80, protocol: HTTP)" Value=MyALB-ELBv2-44R3J0MV1D37
INFO[0056] ApplicationLoadBalancerURL80 Description="ALB URL (port: 80, protocol: HTTP)" Value="http://MyALB-ELBv2-44R3J0MV1D37-943334334.us-west-2.elb.amazonaws.com:80"
Using curl we can verify the newly provisioned Application Load Balancer behavior. The default lambda function echoes the incoming request and is available at the ALB URL basepath:
curl http://MyALB-ELBv2-44R3J0MV1D37-943334334.us-west-2.elb.amazonaws.com
returns
{
"httpMethod": "GET",
"path": "/",
"headers": {
"accept": "*/*",
"host": "MyALB-ELBv2-44R3J0MV1D37-943334334.us-west-2.elb.amazonaws.com",
"user-agent": "curl/7.54.0",
"x-amzn-trace-id": "Root=1-5d507bf4-ca98d1ad44ac0fe56ec6a9ae",
"x-forwarded-for": "24.17.9.178",
"x-forwarded-port": "80",
"x-forwarded-proto": "http"
},
"requestContext": {
"elb": {
"targetGroupArn": "arn:aws:elasticloadbalancing:us-west-2:123412341234:targetgroup/MyALB-ALBDe-1OJX6J3VGX369/1dab61286efaebb6"
}
},
"isBase64Encoded": false,
"body": ""
}
The conditional lambda function behavior is exposed at /newhello as in:
curl http://MyALB-ELBv2-44R3J0MV1D37-943334334.us-west-2.elb.amazonaws.com/newhello
Some other handler