ALB with a Lambda Target
- CloudFormation template: yaml
AWSTemplateFormatVersion: 2010-09-09 Description: A public ALB with a Lambda Function target. Transform: AWS::Serverless-2016-10-31
Overview
This CloudFormation template creates an internet-facing ALB that invokes a
Lambda Function. That is, you can curl
the ALB and see a response from the
Lambda Function.
Prerequisets
- ../network/public.html deployed with the same
DeploymentName
Parameters
Parameters:
Param | Value |
---|---|
DeploymentName | test |
DeploymentName
DeploymentName: Type: String Description: A name for this deployment
A deployment is a deployed application, potentially comprised of many
CloudFormation stacks. This is sometimes called an "environment", but that is an
overloaded and confusing term. Use the DeploymentName
to indicate which
logical deployment a stack belongs to.
If a deployment is completely specified by exactly one CloudFormation template,
the DeploymentName
and the AWS::StackName refer to the same things. In that
case, consider not using a DeploymentName
parameter.
Resources
Resources:
ALB (internet-facing)
If you want the ALB to be reachable from the internet, the ALB must:
- Use an internet-facing scheme
- Be associated with public subnets
Why can't we use private subnets?
ALBs can be associated with private subnets, but in this case that would break everything. In AWS-land, only an Internet Gateway can provide a route from the internet into a Subnet, not an ALB. Maybe that isn't the best design in the world, but it is what it is.
Some further explanation:
An ALB has a couple of IP addresses (try host $ALB_DNS_NAME
). If you associate
the ALB with two subnets, the ALB has at least 2 IP addresses: one for each
subnet. When the ALB is associated with a subnet, AWS creates an Elastic Network
Interface (ENI) for the ALB in that subnet, and the ENI is assigned a public IP
address (if this is an internet-facing ALB). That IP becomes one of the ALB's
IP addresses!
Now say those ENIs are in private subnets. This is the same situation as when you have an EC2 instance in a private subnet with a "public" (internet-valid) IP address. You can try to query that IP, but you will never reach the instance.
If the ENI is in a private subnet, network traffic to the ENI's IP has no route into the subnet. So, when an ALB is associated with private subnets, querying the ALB from the internet will always hang.
ALB: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: IpAddressType: ipv4 Scheme: internet-facing SecurityGroups: [!Ref AlbSecurityGroup] Subnets: - Fn::ImportValue: !Sub "${DeploymentName}-PublicSubnet1" - Fn::ImportValue: !Sub "${DeploymentName}-PublicSubnet2" Type: application Tags: - Key: Name Value: !Ref AWS::StackName
AlbSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Allow HTTP on port 80 VpcId: {Fn::ImportValue: !Sub "${DeploymentName}-VpcId"} SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 0.0.0.0/0 Tags: - Key: Name Value: !Ref AWS::StackName
ALB Routing
ALBListenerHTTP: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref AlbTargetGroupLambda LoadBalancerArn: !Ref ALB Port: 80 Protocol: HTTP
AlbTargetGroupLambda: Type: AWS::ElasticLoadBalancingV2::TargetGroup DependsOn: LambdaInvokePermissionElb Properties: TargetType: lambda Targets: - Id: !GetAtt HelloWorldFunction.Arn
TODO: Restrict access from only the TargetGroup (using SourceArn
). This is
tricky. See: https://forums.aws.amazon.com/thread.jspa?threadID=307784
LambdaInvokePermissionElb: Type: AWS::Lambda::Permission DependsOn: HelloWorldFunction Properties: FunctionName: !Ref HelloWorldFunction Action: lambda:InvokeFunction Principal: elasticloadbalancing.amazonaws.com
Lambda Function
HelloWorldFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub "${DeploymentName}-Hello-World" InlineCode: | import json def handler(event, context) -> dict: message = {"hello": "world"} response = { "statusCode": 200, "statusDescription": "200 OK", "headers": {"Content-Type": "application/json"}, "body": json.dumps(message), } return response Handler: index.handler Runtime: python3.8 Timeout: 3
Outputs
Outputs: ALB: Description: DNS name for the ALB Value: !GetAtt ALB.DNSName Export: Name: !Sub "${DeploymentName}-ALB"
Testing
Send an HTTP GET request to the ALB.
(nth 1 (assoc "DeploymentName" Params))
ExportName=${DeploymentName}-ALB alb=$(aws cloudformation list-exports \ --query "Exports[?Name=='${DeploymentName}-ALB'].Value" \ --output text) curl http://$alb
{"hello": "world"}