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
"aws:*"does anything, because it is not the name of a service.