Python Lambda Proxy for API Gateway

This is a fun thing I experimented with in AWS, using Python/Lambda to proxy requests from API Gateway.

Suppose you have a service running in a VPC somewhere in AWS. If you don't want to create an EIP or manage a proxy server, you can use API Gateway to send requests to that instance. API Gateway can hit a lambda, which can be configured to run inside your target VPC, and send requests to the real service.

AWS already has an example proxy service written in node but every time I see node it makes me so angry I just want to wrap my head in fly paper and spend a day at the dump. So I wrote my own in python.

Some notes on that.

Poor Man's ACLs

I just pasted my relevant IPs (changed in this example) directly into the Lambda.

That list is just a string that is parsed every time the lambda is run. The advantage of doing it this way is that it lets me keep all the stuff I care about in one place. I don't want to have to go look in some other database for who all is authorized. And I don't have to write that database code. This also let you have a lot more rules than would be allowed in a set of security groups.

One drawback of API Gateway is that it is open to the internet and you cannot specify security groups on API Gateway. To work around this, I am using the sourceIp field of the incoming API Gateway event and am matching that up against a list of authorized IPs.

This could be altered to check for CIDR ranges if someone wanted.

Copy and Paste

I used urllib2 instead of requests so I wouldn't have to upload a zip file and could just lazily manage my code right there in lambda.

Passing Headers

Using API Gateway, some headers will be for api gateway, and some will be for your backend service. The copy_headers bit of code below allows me to copy just a couple headers I'm interested in. I also copy the authentication headers for my backend service, but removed in this example.

Code

from __future__ import print_function

import json
import urllib2
import ssl

print('Loading function')

authorized_ips = """
35.200.100.101 # My VPN endpoint
37.192.125.17  # Build agent 1
37.192.125.18  # Build agent 2
37.192.125.19  # Build agent 3
37.192.125.20  # Build agent 4
"""

target_server = "https://192.168.101.17"

def is_authorized_ip(ip):
    ips = set()
    for x in authorized_ips.strip().split("\n"):
        ips.add(x.split('#')[0].strip())

    return ip in ips

def lambda_handler(event, context):
    print("Got event\n" + json.dumps(event, indent=2))

    if not is_authorized_ip(event['requestContext']['identity']['sourceIp']):
        return { 'statusCode': 403, 'body': 'IP Deny' }

    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE

    req = urllib2.Request(target_server + event['path'])
    if event['body']:
        req.add_data(event['body'])

    # Copy only some headers
    copy_headers = ('Accept', 'Content-Type')

    for h in copy_headers:
        if h in event['headers']:
            req.add_header(h, event['headers'][h])

    out = {}

    try:
        resp = urllib2.urlopen(req, context=ctx)
        out['statusCode'] = resp.getcode()
        out['body'] = resp.read()

    except urllib2.HTTPError as e:

        out['statusCode'] = e.getcode()
        out['body'] = e.read()

    return out

Duct Tape Required

Bring your duct tape, cobble stuff together, and don't ask for permission. Lambda is all about welding little parts of the cloud together. But the parts you build out of it might actually work most of the time as opposed to a 5 year old Ubuntu server chock full of weird cron jobs. At least there is a whole class of administration and stuff you can do away with.

Overall

AWS Lambda / API Gateway. So many neat things you can do with that.

Comments

Pages

Tags