01 April 2023
(yes, that is a picture of my breakfast)
Today, AWS has released Amazon VPC Lattice to General Availability. This post walks through creating a simple VPC Lattice service using CloudFormation, and takes a look at the service overall.
VPC Lattice was my #1 favourite announcement of AWS re:Invent 2022, so I’m excited to see it released today. As of the time of writing, it’s available in US East (Ohio), US East (N. Virginia), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Asia Pacific (Tokyo), and Europe (Ireland).
VPC Lattice is a service that enables you to connect clients to services within a VPC. It is very similar to AWS PrivateLink (also known as private VPC Endpoints), but with a key difference.
Whilst PrivateLink works by placing Elastic Network Interfaces within your subnet, which your clients can hit to tunnel network traffic through to the destination service, VPC Lattice works by exposing endpoints as link-local addresses. Link-local addresses are (generally) only accessible by software that runs on the client instance itself.
AWS has carved out the range 169.254.171.0/24
for VPC Lattice’s use, typically routing directly to 169.254.171.0
(there’s also an IPv6 equivalent). This is not the first network that AWS exposes via link-local addresses. You may know of:
169.254.169.254
169.254.169.253
169.254.170.2
169.254.169.123
Generally, these endpoints are automatically available to clients within the VPC network without any special routing or security rules. VPC Lattice differs from this slightly, as it requires Security Groups and NACLs to allow traffic to and from the VPC Lattice data plane at 169.254.171.0/24
on whichever port the destination service exposes. I was pretty surprised by this requirement when I saw it as it’s the first link-local address to need this, but it does give network administrators some basic control. Generally, it’s advised to use a managed prefix list instead of the exact range above, as it’s subject to change.
Targets which VPC Lattice connects to closely match that of load balancing target groups, including EC2 instances, VPC IP addresses (both IPv4 and IPv6), Lambda functions, and ALBs. An EKS-specific target type is in private beta as of the time of writing.
For this walkthrough, we’ll discuss the various components needed for a VPC Lattice setup. For simplicity, we’ll be creating a Lambda function as a client (initiates a HTTPS request), and another Lambda function as a server (responds to the HTTPS request). If you want to skip ahead, here’s the completed template.
Let’s begin by creating a basic VPC. The VPC will have two private subnets, but we won’t add any direct routing between them. For simplicity, we’ll also skip adding Network ACLs.
Resources:
# Basic VPC
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.0.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private Subnet (Source Subnet)
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private Subnet (Destination Subnet)
AvailabilityZone: !Select
- 1
- Fn::GetAZs: !Ref AWS::Region
RouteTablePrivate1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private Route Table (Source Subnet)
RouteTablePrivate1Association1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate1
SubnetId: !Ref PrivateSubnet1
RouteTablePrivate2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private Route Table (Destination Subnet)
RouteTablePrivate2Association1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate2
SubnetId: !Ref PrivateSubnet2
Next, we’ll create the service itself. The service will be a Lambda function which performs a basic successful response to any requests, whilst including it’s own event payload in its response body. The function will be within the second private subnet within the VPC, and its security group will only have a single inbound rule from the VPC Lattice service on the port in which it serves.
# Inbound Lambda (Service)
InboundLambdaFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Policies:
- PolicyName: root
PolicyDocument:
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- xray:PutTraceSegments
- xray:PutTelemetryRecords
- ec2:CreateNetworkInterface
- ec2:DescribeNetworkInterfaces
- ec2:DeleteNetworkInterface
Resource: '*'
InboundLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt InboundLambdaFunctionRole.Arn
TracingConfig:
Mode: Active
Runtime: python3.9
Timeout: 10
Code:
ZipFile: |
import os
import json
import http.client
def handler(event, context):
print(event)
return {
"statusCode": 200,
"body": json.dumps({
"success": "true",
"capturedEvent": event
}),
"headers": {
"Content-Type": "application/json"
}
}
VpcConfig:
SecurityGroupIds:
- !Ref InboundLambdaFunctionSecurityGroup
SubnetIds:
- !Ref PrivateSubnet2
InboundLambdaFunctionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for InboundLambdaFunction
VpcId: !Ref VPC
SecurityGroupEgress: []
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 169.254.171.0/24 # should be the prefix list instead, this'll work though
GroupName: demo-inboundsg
Next up, we’ll create the components of the VPC Lattice service itself. This includes:
To keep things simple, we’re not adding an auth policy for the service network or the service itself.
# VPC Lattice
VPCLatticeServiceNetwork:
Type: AWS::VpcLattice::ServiceNetwork
Properties:
Name: demo-servicenetwork
AuthType: NONE
VPCLatticeServiceNetworkSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for service network access
VpcId: !Ref VPC
SecurityGroupEgress: []
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !GetAtt VPC.CidrBlock
GroupName: demo-servicenetworksg
VPCLatticeServiceNetworkVPCAssociation:
Type: AWS::VpcLattice::ServiceNetworkVpcAssociation
Properties:
SecurityGroupIds:
- !Ref VPCLatticeServiceNetworkSecurityGroup
ServiceNetworkIdentifier: !Ref VPCLatticeServiceNetwork
VpcIdentifier: !Ref VPC
VPCLatticeService:
Type: AWS::VpcLattice::Service
Properties:
Name: demo-service
AuthType: NONE
VPCLatticeServiceNetworkServiceAssociation:
Type: AWS::VpcLattice::ServiceNetworkServiceAssociation
Properties:
ServiceNetworkIdentifier: !Ref VPCLatticeServiceNetwork
ServiceIdentifier: !Ref VPCLatticeService
VPCLatticeListener:
Type: AWS::VpcLattice::Listener
Properties:
Name: demo-listener
Port: 443
Protocol: HTTPS
ServiceIdentifier: !Ref VPCLatticeService
DefaultAction:
Forward:
TargetGroups:
- TargetGroupIdentifier: !Ref VPCLatticeTargetGroup
Weight: 100
VPCLatticeTargetGroup:
Type: AWS::VpcLattice::TargetGroup
Properties:
Name: demo-targetgroup
Type: LAMBDA
Targets:
- Id: !GetAtt InboundLambdaFunction.Arn
It’s important to note that by associating the service network to the VPC, there are routes created within the VPCs route table to correctly send traffic destined towards 169.254.171.0/24
to the VPC Lattice service.
The target group also automatically adds a resource-based policy statement to the Lambda function for you (some other services require you to explicitly add an AWS::Lambda::Permission
).
Finally, we’ll create the client which will send requests to the VPC Lattice service. Again, this will be driven via a basic Lambda function. Note that this time, the security group requires an outbound rule towards the VPC Lattice service.
# Outbound Lambda (Client)
OutboundLambdaFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Policies:
- PolicyName: root
PolicyDocument:
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- xray:PutTraceSegments
- xray:PutTelemetryRecords
- ec2:CreateNetworkInterface
- ec2:DescribeNetworkInterfaces
- ec2:DeleteNetworkInterface
Resource: '*'
OutboundLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt OutboundLambdaFunctionRole.Arn
TracingConfig:
Mode: Active
Runtime: python3.9
Environment:
Variables:
ENDPOINT: !GetAtt VPCLatticeServiceNetworkServiceAssociation.DnsEntry.DomainName
Timeout: 10
Code:
ZipFile: |
import os
import json
import http.client
def handler(event, context):
conn = http.client.HTTPSConnection(os.environ["ENDPOINT"])
conn.request("POST", "/", json.dumps(event), {
"Content-Type": 'application/json'
})
res = conn.getresponse()
data = res.read()
print(data.decode("utf-8"))
VpcConfig:
SecurityGroupIds:
- !Ref OutboundLambdaFunctionSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
OutboundLambdaFunctionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for OutboundLambdaFunction
VpcId: !Ref VPC
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 169.254.171.0/24 # should be the prefix list instead, this'll work though
SecurityGroupIngress: []
GroupName: demo-outboundsg
Now that our template is done, we can deploy it via CloudFormation. If you got stuck anywhere, try the pre-made version here.
Once deployed, navigate to the Lambda console and find the function named something similar to “OutboundLambdaFunction”. Create a test event using any JSON object and invoke it. You should see the results from the service come back to you by observing the logs.
It’s worth noting that the pricing model for VPC Lattice is different to that of PrivateLink and will probably end up costing you more overall. For N. Virginia, a PrivateLink service costs $0.01/hour per availability zone, plus $0.01/GB with volume discounts. For the same region, a VPC Lattice service costs $0.025/hour regardless of AZs, plus $0.025/GB with no volume discounts, plus $0.10 per million requests (with the first 300k requests per hour free).
I’m interested to see how architectures will evolve with this new technology. Whilst PrivateLink remains more affordable and already widespread, I can see architects reaching for this new technology to improve their security posture and reduce the load on networking engineers.
If you liked what I’ve written, or want to hear more on this topic, reach out to me on Twitter at @iann0036.