Configuring an AWS VPC to Include Lambda Resolvers with a Fixed IP

2023-02-27

#cloud#backend
Configuring an AWS VPC to Include Lambda Resolvers with a Fixed IP

This is a series of articles about setting up a complex Serverless backend infrastructure with AWS SAM and CloudFormation.

Here is the index of all the articles in case you want to jump to any of them:

1. Setup of AppSync and API Gateway with Multiple AWS Cognito User Pools

2. Configuring S3 Buckets with Permissions and Access Roles in AWS Cognito AuthRole

3. Intro to DynamoDB Resolvers for AppSync Implementation

4. Intro to Lambda Resolvers for AppSync Implementation

  1. Configuring an AWS VPC to Include Lambda Resolvers with a Fixed IP

6. Intro to Pipeline Resolvers for AppSync Implementation

7. Handling Lambda Resolver Timeouts with SNS Messages


Your Lambda needs to call a third-party API that whitelists IPs. But Lambda doesn't have a fixed IP — every invocation could come from a different address. The whitelist rejects you, and you're stuck.

The fix is a VPC with a NAT Gateway. Route your Lambda's traffic through a single Elastic IP, and every outbound request looks the same to the outside world.

The Setup

You need five things: a VPC, public subnets with an Internet Gateway, a private subnet, a NAT Gateway with an Elastic IP, and the Lambda sitting in the private subnet. The private subnet routes through the NAT, which gives you a static IP. The public subnets handle Lambdas that don't need internet access.

Resources:

  # VPC
  MyVPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true

  # Subnets
  PublicSubnetA:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: "10.0.1.0/24"
      MapPublicIpOnLaunch: true

  PublicSubnetB:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: "10.0.2.0/24"
      MapPublicIpOnLaunch: true

  # Internet Gateway for public internet access
  InternetGateway:
    Type: "AWS::EC2::InternetGateway"

  GatewayAttachment:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref MyVPC
      InternetGatewayId: !Ref InternetGateway

  # Route Table for public subnets
  PublicRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref MyVPC

  PublicRoute:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

  SubnetRouteTableAssociationA:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTable

  SubnetRouteTableAssociationB:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PublicSubnetB
      RouteTableId: !Ref PublicRouteTable

  # Elastic IP for NAT Gateway
  NatEIP:
    Type: "AWS::EC2::EIP"

  # NAT Gateway
  NatGateway:
    Type: "AWS::EC2::NatGateway"
    Properties:
      AllocationId: !GetAtt NatEIP.AllocationId
      SubnetId: !Ref PublicSubnetA

  # Private Subnet for Lambda
  PrivateSubnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: "10.0.3.0/24"

  # Route Table for private subnet
  PrivateRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref MyVPC

  PrivateRoute:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NatGateway

  PrivateSubnetRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  # Security Group to allow Lambda to communicate within the VPC
  LambdaSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Enable Lambda to communicate within the VPC"
      VpcId: !Ref MyVPC

  # Lambda Function inside VPC without Internet Access
  MyInternalLambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Handler: "index.handler"
      Role: !GetAtt LambdaExecutionRole.Arn
      FunctionName: "MyVPCBasedLambda"
      Code:
        S3Bucket: "myBucket"
        S3Key: "code/myLambda.zip"
      Runtime: "nodejs14.x"
      VpcConfig:
        SecurityGroupIds:
          - !GetAtt LambdaSecurityGroup.GroupId
        SubnetIds:
          - !Ref PublicSubnetA
          - !Ref PublicSubnetB

  # Lambda Function inside VPC with Internet Access
  MyInternalLambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Handler: "index.handler"
      Role: !GetAtt LambdaExecutionRole.Arn
      FunctionName: "MyVPCBasedLambda"
      Code:
        S3Bucket: "myBucket"
        S3Key: "code/myLambda.zip"
      Runtime: "nodejs14.x"
      VpcConfig:
        SecurityGroupIds:
          - !GetAtt LambdaSecurityGroup.GroupId
        SubnetIds:
          - !Ref PrivateSubnet

  # IAM Role for Lambda Execution
  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "lambda.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "LambdaExecutionPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "arn:aws:logs:*:*:*"

The key distinction: Lambdas in the public subnets can communicate within the VPC but have no internet access. Lambdas in the private subnet route through the NAT Gateway — they get internet access with a fixed IP.

Code examples are simplified to illustrate the approach. Some adjustments may be needed for production.


Next UP: Part 6. Intro to Pipeline Resolvers for AppSync Implementation