Follow me on X (former twitter) ๐
TL;DR
In this article, we will learn how to assign a static IP to a Lambda for free using the AWS CDK ๐ธ
The problem with Lambdas and static IPs
It's actually easy to assign a static IP to a Lambda, as shown by this article.
โ The only caveat is that we need to use a NAT Gateway and it is quite expensive ($30/month).
A few weeks ago I stumbled across this great article by Yan Cui. He explains how to outsmart AWS and exploit the resources they create in the background for us.
Basically :
- every time you create a Lambda in a vpc, AWS creates an ENI (Elastic Network Interface)
- we can programmatically extract the ENI ID
- we can then attach an Elastic IP to the corresponding ENI
The idea in Yan's article is amazing but the Infrastructure as Code implementation is a bit complex. Here is how I was able to do it using the AWS CDK.
Deploying a Lambda with a static IP using ENIs
Step 1: Create a Lambda in a VPC
First, we need to create a Lambda in a VPC. It should be in a public subnet so that it can access the internet.
import * as cdk from 'aws-cdk-lib';
const vpc = new cdk.aws_ec2.Vpc(this, 'Vpc', {
natGateways: 0,
});
// we'll see later why we need this
const sg = new cdk.aws_ec2.SecurityGroup(this, 'SecurityGroup', { vpc });
const func = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'TestFunc', {
vpc,
// cdk wants to make sure that the lambda can access the internet
allowPublicSubnet: true,
// the lambda needs to be in a public subnet
vpcSubnets: { subnets: vpc.publicSubnets },
securityGroups: [sg],
// ...
});
If your lambda already exists, make sure you move it to a public subnet.
Step 2: Extract the Elastic Network Interface ID
Now that we have a Lambda in a VPC, we need to find the ENI ID aws created automatically.
Let's try like this
const eni = func.connections.securityGroups[0].node.defaultChild as cdk.aws_ec2.CfnNetworkInterface;
Well actually, it doesn't work. ๐
The ENI is not created yet when the CDK is executed. The resource is created by the AWS Cloudformation service when the lambda is deployed. We need to find a way to extract the ENI ID after the lambda has been created during the rest of the deployment.
The solution is to use a custom resource. It allows us to execute a lambda during the deployment, and fetch the ENI ID using the AWS SDK. We can do that easily using the AwsCustomResource
construct.
const cr = new cdk.custom_resources.AwsCustomResource(this, 'customResource', {
onCreate: {
physicalResourceId: cdk.custom_resources.PhysicalResourceId.of(
// adds a dependency on the security group and the subnet
`${sg.securityGroupId}-${vpc.publicSubnets[0].subnetId}-CustomResource`,
),
service: 'EC2',
action: 'describeNetworkInterfaces',
parameters: {
Filters: [
{ Name: 'interface-type', Values: ['lambda'] },
{ Name: 'group-id', Values: [sg.securityGroupId] },
{ Name: 'subnet-id', Values: [vpc.publicSubnets[0].subnetId] },
],
},
},
policy: cdk.custom_resources.AwsCustomResourcePolicy.fromSdkCalls({
resources: cdk.custom_resources.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});
// adds a dependency on the lambda function
cr.node.addDependency(func);
// we can now extract the ENI ID
const eniId = cr.getResponseField('NetworkInterfaces.0.NetworkInterfaceId');
In the snippet above we are calling describeNetworkInterfaces
function of the AWS SDK. We only keep the ENIs attached to a Lambda, in my security group and my public subnet. Adding our lambda as a dependency ensures that the custom resource is executed after the lambda is created.
Step 3: Attach an Elastic IP to the ENI - (the easiest one)
Now that we have the ENI ID, we can attach an Elastic IP to it
// const eniId = cr.getResponseField("NetworkInterfaces.0.NetworkInterfaceId");
const eip = new cdk.aws_ec2.CfnEIP(subnet, "EIP", { domain: "vpc" });
new cdk.aws_ec2.CfnEIPAssociation(this, "EIPAssociation", {
networkInterfaceId: eniId
allocationId: eip.attrAllocationId,
});
Step 4: Let's put it all together
We now have all the pieces we need to create a lambda with a static IP. We were able to do it for a single subnet, we just need to repeat the process for all the public subnets our lambda needs to be in.
You can find the full code here.
Key takeaways
- We can use attach a static IP to a Lambda for FREE using ENIs ๐ธ
- We can use the
AwsCustomResource
construct to perform AWS SDK calls during the deployment
Make sure you enjoy your free static IP while it lasts. Elastic IPs will cost $3/month as of January 2024.
Although this is great for reducing your costs, you should not use it for production environments. AWS might change the way they manage ENIs in the future and break your infrastructure. Furthermore, if you're Lambda is not used in a while AWS will delete the ENI. To avoid this you can setup a cron job to keep the ENI.
Top comments (6)
I think using lightsail with static IP is better
Any idea how can I use AWS SDK with Google App Script? I want to call a lambda without AWS gateway constraint of 29 second. Please help if you know.
I'm not familiar with Google App Script but depending on your requirements and need for authentication, you could use function URLs - docs.aws.amazon.com/lambda/latest/...
Alternatively you could use the API Gateway call to start your process (potentially by writing a message to a queue or triggering an event which are hooked up to the lambda that actually does the work) and return. Then you would poll a status lambda for the progress of the job.
Both would give you up to 15 minutes of runtime. In the 2nd example, you could split this up over multiple lambda invocations according to your use case giving you an unlimited amount of time to complete
Tanks @rcoundon for the answer
I totally agree
Wow! Lovely post.
Any idea on how that could be used for ECS containers running on specify IP as well?
I build serverless application most of the time so I'm no expert on EC2.
The EIPAssociation I'm using can be used on an ec2 instance directly and you won't need to use ENIs.
Let me know if it works for you