Create Lambda Functions in a VPC in AWS CDK

avatar

Borislav Hadzhiev

Fri May 07 20215 min read

banner

Photo by S Migaj

A complete example of provisioning a lambda function with access to the internet in a VPC in AWS CDK.

Creating Lambda Functions in a VPC in AWS CDK #

In this article we are going to provision a lambda function in a VPC and enable it to access the internet.

There are a couple of things we have to do to give lambda functions created in a VPC internet access:

  • the function has to have permissions to create and manage elastic network interfaces (virtual network cards)
  • the function has to be placed in a private subnet with a route table rule pointing to a NAT gateway or NAT instance. The NAT Gateway has to be provisioned in a public subnet and has to have a public IP address in order to access the internet through the VPC's Internet Gateway.
  • the function's security group has to allow the necessary outbound access
The code for this article is available on GitHub

Let's create the VPC and lambda function:

lib/cdk-starter-stack.ts
import * as ec2 from '@aws-cdk/aws-ec2';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import * as path from 'path';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'my-cdk-vpc', {
      cidr: '10.0.0.0/16',
      natGateways: 1,
      maxAzs: 3,
      subnetConfiguration: [
        {
          name: 'private-subnet-1',
          subnetType: ec2.SubnetType.PRIVATE,
          cidrMask: 24,
        },
        {
          name: 'public-subnet-1',
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24,
        },
      ],
    });

    const lambdaFunction = new lambda.Function(this, 'lambda-function', {
      runtime: lambda.Runtime.NODEJS_14_X,
      // ๐Ÿ‘‡ place lambda in the VPC
      vpc,
      // ๐Ÿ‘‡ place lambda in Private Subnets
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE,
      },
      memorySize: 1024,
      timeout: cdk.Duration.seconds(5),
      handler: 'index.main',
      code: lambda.Code.fromAsset(path.join(__dirname, '/../src/my-lambda')),
    });
  }
}

Let's go over the code snippet.

  1. we created a VPC with a PUBLIC and a PRIVATE subnet groups. We will launch our lambda function in private subnets. Note that the VPC will provision 1 NAT Gateway, which will allow our lambda to access the internet from a PRIVATE subnet
NAT Gateways have an hourly billing rate of about $0.045 in the us-east-1region.
  1. we created a lambda function and placed it in the VPC. Note that we've narrowed down the subnets to PRIVATE.

Lambda functions provisioned in a PUBLIC subnet don't get assigned a public IP address and don't have access to the internet.

We didn't add a role to the lambda function, or edit its default role.

Lambda creates an elastic network interface (virtual network card) for each (private) subnet in the VPC, so it has to have the necessary permissions to create, describe and delete network interfaces.

The necessary permissions are included in the AWSLambdaVPCAccessExecutionRole managed policy, which CDK attaches to lambdas launched in a VPC automatically.

We haven't explicitly provided a security group to the lambda function, so CDK will also create a default security group for us.

The default security group has a single inbound rule:

TypeProtocolPortSource
All TrafficAllAlldefault-SG-id

And the following outbound rule:

TypeProtocolPortSource
All TrafficAllAll0.0.0.0/0

The default security group allows all outbound traffic. Since security groups are stateful, an outbound request that we've made to the internet, automatically gets permissions for the response from the other direction.

In our case the default security group will do. If you want to read more about creating security groups in CDK, check out my other article - Security Group Example in AWS CDK - Complete Guide.

Let's add the code for the lambda function at src/my-lambda/index.js:

src/my-lambda/index.js
const fetch = require('node-fetch');

async function main(event) {
  try {
    const res = await fetch('https://randomuser.me/api');
    const resJson = await res.json();

    console.log('api response ๐Ÿ‘‰', JSON.stringify(resJson, null, 4));
    return {body: JSON.stringify(resJson), statusCode: 200};
  } catch (error) {
    return {body: JSON.stringify({error})};
  }
}

module.exports = {main};

The function makes a request to an API and returns the response. We've used the node-fetch package, so we have to install it:

shell
cd src/my-lambda
npm init -y
npm install node-fetch
cd ../../

Let's deploy the lambda function and the VPC and test if our function has access to the internet:

shell
npx cdk deploy

After a deployment, we can see that the lambda has been launched in a VPC, and is associated to private subnets only.

lambda in VPC

The route tables associated to our private subnets have a route that points all non intra-VPC traffic out to our NAT Gateway:

routes private subnet

CDK has also automatically attached the AWSLambdaVPCAccessExecutionRole managed policy to our lambda's role:

vpc lambda role

Our public subnets route all non intra-VPC traffic to the Internet Gateway:

routes public subnet

Everything seems to be in place for our Lambda function launched in a VPC to have internet access. Let's test the function via the Lambda management console:

vpc lambda response

Our lambda function successfully queried the remote API and got the response.

The CloudWatch logs are a bit more readable:

vpc lambda response cloudwatch

Either way, we successfully provisioned a lambda function, that can access the internet, in a VPC.

The function is created in PRIVATE subnets of the VPC. The route tables associated to our private subnets have a route that points to a NAT Gateway, which enables our lambda to access the internet.

CDK did quite a bit of the heavy lifting for us:

  • provided the glue logic for all of the VPC components
  • created a role for our lambda function and attached permissions to it, that enable ENI creation and management
  • created a security group for our lambda function
If your lambda function is placed in a VPC and doesn't need access internet access, you can set the allowPublicSubnet to true on the Functionconstructor, to acknowledge the limitation. Placing your VPC lambda in a public subnet without setting this prop to `true` returns a validation error.

Clean up #

To delete the resources we've provisioned, execute the destroy command:

shell
npx cdk destroy
Don't forget to destroy the stack, because we provisioned a NAT Gateway, which has an hourly billing rate.

Further Reading #

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee