Playing with AWS Lambda

I started playing with AWS Lambda tonight. Eventually, I’d like to use Lambda and the API gateway to provide a sort of DDNS (dynamic DNS); a script should run on my home server, touch the API gateway, and Lambda should reprogram an address in Route 53 to match whatever was used for the origin IP. That involves passing a few parameters around, so I figured a good first step was to write a Lambda to collect any arguments and email them to me. Should be easy, right? There’s even a quick example on sending email in the Python smtplib docs. Should be easy, right?

The joys of Amazon email-handling

It turns out Amazon’s Lambda environment doesn’t allow connections to just any SMTP server; you need to use one of the servers that provide AWS’ SES (Simple Email Service). Amazon provides several servers, one per region; use what’s closest. Connections to all other mail servers will fail with a generic “Connection closed” message (presumably Amazon is simply resetting these connections as they’re opened).

Once I was able to open a server connection, I started getting failures due to a lack of authentication. Amazon charges by the email, so I needed to create an IAM user to handle my mail sending (and add Python code to turn on STARTTLS and actually log in). I used the SES credential creation wizard, but any IAM user with the AmazonSesSendingAccess inline policy will work as well. In a custom policy, ensure you’ve allowed the ses:SendRawEmail action.

After that, I started getting errors about my sending and receiving email addresses not be “verified”. Turns out Amazon won’t let you send email unless you’ve proven you own the addresses or domains involved. In my case, I verified my domain with Amazon SES, and (since these were largely testing emails) stuck to sending emails to myself.

By the way, the SES verification directions indicate that the verification is region-specific. If you use multiple SES endpoints, you’ll need to verify your email addresses or domains with each one. For domains hosted by Route 53, this process is easy - there’s even a button to propagate records to Route 53 right from the SES console. There’s also support for DKIM, a system for identifying the validity of emails. Must remember to look into that further someday…

Lambda and API Gateway

Creating an API Gateway interface to a Lambda function is pretty easy, once the lambda already exists. Since I wanted to inspect the HTTP headers coming into the gateway, it was important to turn on the Lambda Proxy Integration checkbox. With that, AWS will expect to get back a dictionary (of headers, body, and statusCode) in return. Much of the API Gateway documentation indicates that this should be a JSON dictionary, but if the Lambda is written in Python the Gateway expects a native Python dictionary back.

The API Gateway will pass useful things to your function in the events and context variables. events contains all the HTTP headers, browser info, etc., while context includes any additional information (including meta-parameters, like permissable runtime). In Python, the context variable is actually an object of typeLambdaContext; useful API client data is probably in context.client_context (though that will be None if nothing is passed).

For my purposes, I’m most interested in events['requestContext']['identity']['sourceIP'] - a string containing the client IP address. I’ll turn that into the basis of a dynamic DNS API in the near future. For now, here’s the code I’m using for my test lambda function:

[Test Lambda function] []
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import smtplib, pprint
from email.mime.text import MIMEText
sender = "www@example.org"
recipient = "me@example.org"
server = "email-smtp.us-east-1.amazonaws.com"
username = "ABCDEFGHIJKLMNOPQRST"
password = "At8aj2lvnASuweAvKu3v49siaselinv492nn1jlHFadjJsjsjwl"
port = "587"
def lambda_handler(event, context):
pp = pprint.PrettyPrinter(indent=4)
rdict = {}
rdict['body'] = "Hello from Lambda: <br><pre>" + pp.pformat(event) + "</pre><br><pre>" + pp.pformat(context.client_context) + "</pre>"
rdict['headers'] = { "Content-Type": "text/html" }
rdict['statusCode'] = "200"
msg = MIMEText(pp.pformat(event))
msg['Subject'] = "Test from lambda"
msg['From'] = sender
msg['To'] = recipient
s = smtplib.SMTP(host=server, port=port)
s.starttls()
s.ehlo()
s.login(username, password)
s.sendmail(sender, [recipient], msg.as_string())
s.quit()
return rdict

Replace the sender, recipient, username, and password variables with your own values.

When visiting the API Gateway with a web browser, I get output similar to the following:

[Lambda sample output]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Hello from Lambda:
{ u'body': None,
u'headers': { u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
u'Accept-Encoding': u'gzip, deflate',
u'Accept-Language': u'en-us',
u'Cache-Control': u'max-age=0',
u'CloudFront-Forwarded-Proto': u'https',
u'CloudFront-Is-Desktop-Viewer': u'true',
u'CloudFront-Is-Mobile-Viewer': u'false',
u'CloudFront-Is-SmartTV-Viewer': u'false',
u'CloudFront-Is-Tablet-Viewer': u'false',
u'CloudFront-Viewer-Country': u'US',
u'Cookie': u'regStatus=pre-register; s_dslv=1482545852452; s_fid=023C0FA3C5B564D7-149E1840C1D08425; s_nr=1482545852462-New; s_vn=1514081675457%26vn%3D1',
u'DNT': u'1',
u'Host': u'81i44Fkwn.execute-api.us-east-1.amazonaws.com',
u'Referer': u'https://console.aws.amazon.com/apigateway/home?region=us-east-1',
u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0.2 Safari/602.3.12',
u'Via': u'1.1 9184810928a31c0038199.cloudfront.net (CloudFront)',
u'X-Amz-Cf-Id': u'KNiwjv9wnadjwJFHbWJCjbdbdyyxx==',
u'X-Forwarded-For': u'89.33.210.12, 205.251.252.177',
u'X-Forwarded-Port': u'443',
u'X-Forwarded-Proto': u'https'},
u'httpMethod': u'GET',
u'isBase64Encoded': False,
u'path': u'/',
u'pathParameters': None,
u'queryStringParameters': None,
u'requestContext': { u'accountId': u'175919371',
u'apiId': u'81i44Fkwn',
u'httpMethod': u'GET',
u'identity': { u'accessKey': None,
u'accountId': None,
u'apiKey': None,
u'caller': None,
u'cognitoAuthenticationProvider': None,
u'cognitoAuthenticationType': None,
u'cognitoIdentityId': None,
u'cognitoIdentityPoolId': None,
u'sourceIp': u'89.33.210.12',
u'user': None,
u'userAgent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0.2 Safari/602.3.12',
u'userArn': None},
u'requestId': u'713e07f6-db86-11e6-9fb8-3b48cbecf41e',
u'resourceId': u'lasdu138cjc',
u'resourcePath': u'/',
u'stage': u'Production'},
u'resource': u'/',
u'stageVariables': None}
None

Took a bit of digging, but now I have a nice little URL I can visit that calls a Lambda, prints off the various arguments, and even emails me the event data. Nifty!