Jake Feasel
Jake Feasel

Reputation: 16955

AWS Lambda function using JavaScript SDK always throws timeout error

I am attempting to use the AWS SDK for JavaScript within my AWS Lambda function (NodeJS 6.10 runtime). My ultimate goal is to use this to manage my ECS instances, but for now I'm simply trying to use any part of the API and am failing with each attempt. I have reduced the function to the simplest possible; take a look:

exports.handler = (event, context, callback) => {
    var AWS = require('aws-sdk');
    (new AWS.ECS({"apiVersion": '2014-11-13'})).listClusters({}, (err, data) => {
        if (err) console.log(err, err.stack);
        else     console.log(data);

        callback(null, "DONE");
    })
};

I have given this function an IAM role that has this definition:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "aws:*",
                "ecs:*"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

I have set this function to run within my existing VPC, subnets and security groups. I have increased the timeouts and memory caps beyond all possible needs.

Every execution of this function fails with a timeout exception. I have tried using many different API calls, and even different service APIs, but every attempt to invoke an API within my function always results in a timeout.

I even enabled X-Ray tracing for this function, but by all appearances it seems that nothing leaves the Lambda execution environment - X-Ray reports no activity out to other parts of AWS (ECS, for example).

What have I missed? Why can't I use any of the JS SDK within Lambda?

Upvotes: 1

Views: 1107

Answers (2)

Jake Feasel
Jake Feasel

Reputation: 16955

For posterity, I took John Rotenstein's excellent advise and used the VPC Wizard to get things working. Turns out the pivotal detail has to do with the construction of the NAT gateway.

If you want your Lambda function to have access to the AWS SDK and your VPC resources, you need at least two subnets (one for public and one for private resources). Here are the VPC commands you need to execute, which are similar to what happens with the VPC Wizard:

create a new VPC, or set these env variables to your existing ones

export REGION=us-west-2 #or whatever region you want

export VPC_ID=`aws ec2 create-vpc --cidr-block 10.1.0.0/16 \
    --query Vpc.VpcId --output text`

create an internet gateway for your public resources

export IGW_ID=`aws ec2 create-internet-gateway \
    --query InternetGateway.InternetGatewayId --output text`

aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID

create route tables for your public subnet, using the above IGW

export ROUTE_TABLE_ID_PUBLIC=`aws ec2 describe-route-tables \
    --filter Name=vpc-id,Values=$VPC_ID --query RouteTables[0].RouteTableId --output text`

export SUBNET_ID_PUBLIC=`aws ec2 create-subnet --vpc-id $VPC_ID \
    --cidr-block 10.1.0.0/24 --availability-zone ${REGION}a \
    --query Subnet.SubnetId --output text`

aws ec2 associate-route-table --subnet-id $SUBNET_ID_PUBLIC \
    --route-table-id $ROUTE_TABLE_ID_PUBLIC

aws ec2 create-route --route-table-id $ROUTE_TABLE_ID_PUBLIC \
    --gateway-id $IGW_ID --destination-cidr-block 0.0.0.0/0

create an IP Allocation for a NAT Gateway, for use with a private subnet

export IP_ALLOCATION_ID=`aws ec2 allocate-address --domain vpc \
    --query AllocationId --output text`

export ROUTE_TABLE_ID_PRIVATE=`aws ec2 create-route-table --vpc-id $VPC_ID \
    --query RouteTable.RouteTableId --output text`

export SUBNET_ID_PRIVATE=`aws ec2 create-subnet --vpc-id $VPC_ID \
    --cidr-block 10.1.1.0/24 --availability-zone ${REGION}b \
    --query Subnet.SubnetId --output text`

aws ec2 associate-route-table --subnet-id $SUBNET_ID_PRIVATE \
    --route-table-id $ROUTE_TABLE_ID_PRIVATE

export NAT_GW_ID=`aws ec2 create-nat-gateway --subnet-id $SUBNET_ID_PUBLIC \
    --allocation-id $IP_ALLOCATION_ID --query NatGateway.NatGatewayId --output text`

Wait here a few moments - it takes some time before the NAT Gateway is ready and usable for further commands.

The key detail (that I was unable to find in the AWS documentation) is within that last command, above - that the NAT Gateway must be created with the PUBLIC subnet, even though it is associated with the PRIVATE route table:

create private route table with NAT Gateway

aws ec2 create-route --route-table-id $ROUTE_TABLE_ID_PRIVATE \
    --gateway-id $NAT_GW_ID --destination-cidr-block 0.0.0.0/0

create security groups, etc....

export SECURITY_GROUP_ID=`aws ec2 create-security-group --vpc-id $VPC_ID \
    --group-name mygroup --description "My SG" \
    --query GroupId --output text`

aws ec2 authorize-security-group-ingress --group-id $SECURITY_GROUP_ID \
    --protocol tcp --port 22 --cidr 0.0.0.0/0

aws ec2 authorize-security-group-ingress --group-id $SECURITY_GROUP_ID \
    --cidr 10.1.0.0/16 --protocol all

At this point you should be able to create Lambda functions which are associated with the private subnet, which have access to resources in the VPC and also can make calls out to the Internet (necessary for AWS SDK usage). Here's an example Lambda function which does exactly this:

create IAM roles necessary to execute Lambda functions within your VPC

aws iam create-instance-profile --instance-profile-name testRole

testRole_trust_policy.json:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

export TEST_ROLE_ARN=`aws iam create-role --role-name testRole \
  --assume-role-policy-document file://testRole_trust_policy.json \
  --query Role.Arn --output text`


testRole_policy.json:
{
  "Version": "2012-10-17",
  "Statement": [
      {
          "Effect": "Allow",
          "Action": [
              "lambda:*",
              "ec2:CreateNetworkInterface",
              "ec2:DescribeNetworkInterfaces",
              "ec2:DeleteNetworkInterface"
          ],
          "Resource": [
              "*"
          ]
      }
  ]
}

aws iam put-role-policy --role-name testRole \
  --policy-name testRole --policy-document file://testRole_policy.json

aws iam add-role-to-instance-profile \
  --instance-profile-name testRole \
  --role-name testRole

create a Lambda function using this IAM role

contents of lambda.zip:
- test.js:
    exports.handler = (event, context, callback) => {
        AWS = require('aws-sdk'),
            lambda = new AWS.Lambda({"apiVersion": '2015-03-31'});

        lambda.listFunctions(callback);
    };


aws lambda create-function --function-name testWithVPC \
    --runtime nodejs6.10 --role $TEST_ROLE_ARN \
    --handler test.handler --timeout 10 \
    --zip-file fileb://lambda.zip \
    --vpc-config SubnetIds=$SUBNET_ID_PRIVATE,SecurityGroupIds=$SECURITY_GROUP_ID

execute it and see the results:

aws lambda invoke --function-name testWithVPC with.txt
with.txt:
    {"NextMarker":null,"Functions":[{"FunctionName":"testWithVPC","FunctionArn": ....]}

This is enough to demonstrate the functionality. My project building upon this pattern is available here, for more robust samples: https://github.com/jakefeasel/sqlfiddle3

Upvotes: 0

John Rotenstein
John Rotenstein

Reputation: 269091

Your code worked perfectly fine for me, but here's some things I had to do first:

The above is because the Lambda function requires Internet access to cal the AWS API endpoints. Lambda functions attached to a VPC only have a Private IP address, so they require a NAT Gateway or NAT Instance to have Internet access. See: Internet Access for Lambda Functions

Upvotes: 3

Related Questions