Search code examples
aws-lambdaserverless-frameworksubnetaws-nat-gateway

Allow lambda internet connection inside VPC


I have one lambda inside a VPC. It has 2 subnets (A & B). I need to call an external api inside the lambda. I've been reading about NatGatway and RouteTables but I'm not able to provide internet access (output, from Lambda to the API) to the lambda. I'm using serverless framework.

Here is my serverless file:

serverless.yml

...
provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  memorySize: 256
  iamRoleStatements:
    - Effect: 'Allow'
      Resource: !GetAtt ReceiveOrderQueue.Arn
      Action:
        - 'sqs:SendMessage'
    - Effect: 'Allow'
      Action:
        - 'lambda:InvokeFunction'
      Resource: '*'

  environment:
    NODE_ENV: ${opt:stage, 'dev'}
    DB_NAME: ${self:custom.DB_NAME}
    DB_USER: ${self:custom.DB_USERNAME}
    DB_PASS: ${self:custom.DB_PASSWORD}
    DB_PORT: ${self:custom.DB_PORT}
    DB_HOST: ${self:custom.PROXY_ENDPOINT}
    DATABASE_URL: 'mysql://${self:custom.DB_USERNAME}:${self:custom.DB_PASSWORD}@${self:custom.PROXY_ENDPOINT}:${self:custom.DB_PORT}/${self:custom.DB_NAME}'

plugins:
  - serverless-offline
  - serverless-plugin-include-dependencies

resources:
  - ${file(resources/vpc.yml)}
  - ${file(resources/routing.yml)}
  - ${file(resources/rds.yml)}
  - ${file(resources/rds-proxy.yml)}
  - ${file(resources/receive-order-queue.yml)}

custom:
  ...

functions:
  refresh-api:
    name: update-order-status-${opt:stage, 'dev'} // This is the lambda that needs to call the api
    handler: update-order-status/main.handler
    timeout: 30
    vpc:
      securityGroupIds:
        - !Ref LambdaSecurityGroup
      subnetIds:
        - !Ref SubnetA
        - !Ref SubnetB
    events:
      - httpApi:
          method: ANY
          path: /{proxy+}

  order-receiver:
    name: order-receiver-${opt:stage, 'dev'}
    handler: order-receiver/main.handler
    timeout: 120
    vpc:
      securityGroupIds:
        - !Ref LambdaSecurityGroup
      subnetIds:
        - !Ref SubnetA
        - !Ref SubnetB
    events:
      - sqs:
          arn: !GetAtt ReceiveOrderQueue.Arn
          batchSize: 10

package:
  patterns:
    - '!node_modules/.prisma/client/libquery_engine-*'
    - 'node_modules/.prisma/client/libquery_engine-linux-arm64-*'
    - 'node_modules/.prisma/client/libquery_engine-rhel-openssl-1.0.x.so.node'
    - '!node_modules/prisma/libquery_engine-*'
    - '!node_modules/@prisma/engines/**'

Here are my resources related to VPC and Routing (I don't think that all others be needed to solve this trouble)

routing.yml

Resources:
  RouteTablePublic:
    DependsOn: VPCGA
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: 'Name'
          Value: 'RouteTablePublic'

  RoutePublic:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref RouteTablePublic

  RouteTableAssociationSubnetA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTablePublic
      SubnetId: !Ref SubnetA

  NatGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  NatGatewayA:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref SubnetA

  NatGatewayB:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref SubnetB

  RouteTableAssociationSubnetB:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTablePublic
      SubnetId: !Ref SubnetB

And my vpc.yml

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: ${self:custom.VPC_CIDR}.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: 'Name'
          Value: 'VPC'

  SubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ${self:provider.region}a
      CidrBlock: ${self:custom.VPC_CIDR}.0.0.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: 'Name'
          Value: 'SubnetA'

  SubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ${self:provider.region}b
      CidrBlock: ${self:custom.VPC_CIDR}.0.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: 'Name'
          Value: 'SubnetB'

  LambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupDescription: 'Security group for Lambdas'
      Tags:
        - Key: 'RefreshLambdaSecurityGroup'
          Value: 'LambdaSecurityGroup'

  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'Security group for RDS'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: '0.0.0.0/0'
      Tags:
        - Key: 'RefeshRDSSecurityGroup'
          Value: 'RDSSecurityGroup'

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: 'Name'
          Value: 'InternetGateway'

  VPCGA:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  SQSEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.sqs'
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref LambdaSecurityGroup
      SubnetIds:
        - !Ref SubnetA
        - !Ref SubnetB
      VpcEndpointType: Interface

Thanks in advance <3


Solution

  • This shows that you are creating 2 Subnets and 2 NAT Gateways in them. Both are public subnet and their default gateway points at Internet gateway as expected. When it comes to Lambda, you will need to create Private Subnets (i.e. 2 more subnets) and point their default gateway towards the NAT Gateways.

    Sharing updated file sections below: (excuse any typo's)

    vpc.yml Adding 2 private subnets too

      SubnetC:
        Type: AWS::EC2::Subnet
        Properties:
          VpcId: !Ref VPC
          AvailabilityZone: ${self:provider.region}a
          CidrBlock: ${self:custom.VPC_CIDR}.0.2.0/24
          MapPublicIpOnLaunch: false
          Tags:
            - Key: 'Name'
              Value: 'SubnetC'
    
      SubnetD:
        Type: AWS::EC2::Subnet
        Properties:
          VpcId: !Ref VPC
          AvailabilityZone: ${self:provider.region}b
          CidrBlock: ${self:custom.VPC_CIDR}.0.3.0/24
          MapPublicIpOnLaunch: false
          Tags:
            - Key: 'Name'
              Value: 'SubnetD'
    

    routing.yml Getting Routes setup for 2 private subnets. Since you have 2 NAT Gateways, we will need 2 Private route tables, one for each AZ

      RouteTablePrivateAzA:
        DependsOn: VPCGA
        Type: AWS::EC2::RouteTable
        Properties:
          VpcId: !Ref VPC
          Tags:
            - Key: 'Name'
              Value: 'RouteTablePrivateAzA'
    
      RouteTablePrivateAzB:
        DependsOn: VPCGA
        Type: AWS::EC2::RouteTable
        Properties:
          VpcId: !Ref VPC
          Tags:
            - Key: 'Name'
              Value: 'RouteTablePrivateAzB'
              
      RoutePrivateAzA:
        Type: AWS::EC2::Route
        Properties:
          DestinationCidrBlock: 0.0.0.0/0
          GatewayId: !Ref NatGatewayA
          RouteTableId: !Ref RouteTablePrivateAzA
          
      RoutePrivateAzB:
        Type: AWS::EC2::Route
        Properties:
          DestinationCidrBlock: 0.0.0.0/0
          GatewayId: !Ref NatGatewayB
          RouteTableId: !Ref RouteTablePrivateAzB
        
      RouteTableAssociationSubnetC:
        Type: AWS::EC2::SubnetRouteTableAssociation
        Properties:
          RouteTableId: !Ref RouteTablePrivateAzA
          SubnetId: !Ref SubnetC
          
      RouteTableAssociationSubnetD:
        Type: AWS::EC2::SubnetRouteTableAssociation
        Properties:
          RouteTableId: !Ref RouteTablePrivateAzB
          SubnetId: !Ref SubnetD
    

    serverless.yml Creating Lambda in Private Subnets

      order-receiver:
        name: order-receiver-${opt:stage, 'dev'}
        handler: order-receiver/main.handler
        timeout: 120
        vpc:
          securityGroupIds:
            - !Ref LambdaSecurityGroup
          subnetIds:
            - !Ref SubnetC
            - !Ref SubnetD
    

    With the above flow will be:

    Lambda [Private Subnet] > NAT GW [Public Subnet] > IGW > Internet