AWS Application Load BalancerのターゲットにLambda(Python)を指定してみた

11月26日から開催されています***AWS re:Invent 2018***で、Serverless系の新ネタが少ないと思ってましたが、ようやくきました。

[アプリケーションロードバランサー(ALB)のターゲットにAWS Lambdaが選択可能になりました](
https://aws.amazon.com/jp/blogs/news/lambda-functions-as-targets-for-application-load-balancers/)

これまでは、Lambdaを外部から呼び出す場合は、API Gatewayからが一般的だと想いますが、ALBから呼び出すことができるようになりました。
API Gatewayを利用して実装する場合、URL遷移させたいWebアプリの実装が難しかったと思います。しかし、ALB経由でLambdaを実行できるようになったことで、URL遷移のアプリ実装も簡単になると思います。

既に利用できるようになっていたので試してみました。

## まずはパラメータ調査用Lambda関数を用意しました
どんなパラメータがLambda関数に渡ってくるのか調査するために、Cloudwatch logsで確認します。ブログに書かれていたソースを参考に以下のようにしました。

```lambda_function.py
import logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

def lambda_handler(event, context):

    logger.debug( event )    
    
    response = {
        "statusCode": 200,
        "isBase64Encoded": False,
        "headers": {
            "Content-Type": "text/html; charset=utf-8"
        }
    }
    
    response['body'] = """<html>
    <head>
        <meta charset="UTF-8">
        <title>Hello World!</title>
    </head>
    <body>
    <p>Hello World!</p>
    </body>
    </html>"""
    
    return response
```

## ALBを作成しました
Lambda関数を作成した後で、ALBの設定をしました。
ターゲットグループの作成画面に、これまではなかった、Lambda関数が指定できるようになってます。ここで、先程作成したLambda関数を選択します。
![target.png](https://qiita-image-store.s3.amazonaws.com/0/77333/783437ad-7162-d4d3-86f9-fcc02088001e.png)
ターゲットグループ以外はこれまでのALB作成と変わらなかったと思います。

## ALBにアクセスしてみました

ALB作成後、`curl http://＊＊＊.ap-northeast-1.elb.amazonaws.com/lambdatest/?a=b&c=d`でアクセスし、Cloudwatch logsに出力されているeventパラメータを調べたみたところ、以下の出力がされていました（一部マスクしています）。
curlの出力は、HTMLが出力されていました。

```
{'requestContext': 
    {'elb': {
        'targetGroupArn': 'arn:aws:elasticloadbalancing:ap-northeast-1:***:targetgroup/TestLoggingLambda/***'
        }
    }, 
    'httpMethod': 'GET', 
    'path': '/lambdatest/', 
    'queryStringParameters': {'a': 'b', 'c': 'd'}, 
    'headers': {
            'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 
            'accept-encoding': 'gzip, deflate', 
            'accept-language': 'ja,en-US;q=0.9,en;q=0.8,th;q=0.7', 
            'connection': 'keep-alive', 
            'host': '***.ap-northeast-1.elb.amazonaws.com', 
            'upgrade-insecure-requests': '1', 
            'user-agent': '***', 
            'x-amzn-trace-id': '***'', 
            'x-forwarded-for': '***'', 
            'x-forwarded-port': '80', 
            'x-forwarded-proto': 'http'
    }, 
    'body': '', 
    'isBase64Encoded': False
}
```
GETパラメータは、`queryStringParameters`に格納されるようです。リクエストヘッダーも取得できていました。

### POSTでアクセスしてみた
`curl -F "hoge=fuga" -F "form2=テスト" http://***.ap-northeast-1.elb.amazonaws.com/lambdatest/`でアクセスしてみたところ、body部分にbase64encodeされた状態でデータが格納されているようです。

```
{'requestContext': 
    {'elb': {
        'targetGroupArn': 'arn:aws:elasticloadbalancing:ap-northeast-1:***:targetgroup/TestLoggingLambda/***'
        }
    }, 
    'httpMethod': 'POST', 
    'path': '/lambdatest/', 
    'queryStringParameters': {}, 
    'headers': {
        'accept': '*/*', 
        'content-length': '246', 
        'content-type': 'multipart/form-data; boundary=------------------------66c2ae32e35a634a', 
        'expect': '100-continue', 
        'host': '***.ap-northeast-1.elb.amazonaws.com', 
        'user-agent': ''***'',', 
        'x-amzn-trace-id': ''***'',', 
        'x-forwarded-for': ''***'',', 
        'x-forwarded-port': '80', 
        'x-forwarded-proto': 'http'
    }, 
    'body':'LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS02NmMyYWUzMmUzNWE2MzRhDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImhvZ2UiDQoNCmZ1Z2ENCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tNjZjMmFlMzJlMzVhNjM0YQ0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJmb3JtMiINCg0K44OG44K544OIDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTY2YzJhZTMyZTM1YTYzNGEtLQ0K', 
    'isBase64Encoded': True
}
```
## GETとPOSTでパラメータを取得するサンプル
Lambda関数に渡ってくるパラメータがわかったので、GETとPOSTの場合でサンプルプログラムを書いてみました。POSTデータのデコードは面倒だったので、Pythonのcgiモジュールを使っています。

```lambda_function.py
import base64
import io
import cgi


def lambda_handler(event, context):
    
    response = {
        "statusCode": 200,
        "isBase64Encoded": False,
        "headers": {
            "Content-Type": "text/html; charset=utf-8"
        }
    }

    response['body'] = """<html>
    <head>
        <meta charset="UTF-8">
        <title>Hello World!</title>
    </head>
    <body>
    <p>Hello World!</p>"""
    
    if event['httpMethod'] == "GET":
        for k,v in event['queryStringParameters'].items():
           response['body'] += "<p>" + k + " : " + v + "</p>"
           
    elif event['httpMethod'] == "POST":
        fp = io.BytesIO(base64.b64decode(event['body']))
        environ = {'REQUEST_METHOD': 'POST'}
        headers = {
            'content-type': event['headers']['content-type'],
            'content-length': event['headers']['content-length']
        }
        form = cgi.FieldStorage(fp=fp, environ=environ, headers=headers)
        
        for f in form.list:
            response['body'] += "<p>" + f.name + " : " + f.value + "</p>"
        
    response['body'] += """<p>ALBからLambdaを呼び出しています</p>
    </body>
    </html>"""
    
    return response
```
参考にさせていただいて記事：[[Python] POSTされたmultipart/form-dataをFieldStorageでパースする](https://qiita.com/ykarakita/items/8a9038597353f0615bbc)

## 構成を考えてみると
これまでの結果、GETやPOSTでやり取りするページは作成できそうです。これを踏まえ、構成を考えてみると、静的ページは、CloudFront+S3で表示をさせ、動的処理が必要なページだけ、CloudFront+ALB+Lambda+(あとは必要なサービス）とすればEC2を利用しなくても大抵のサイトは作成できるような気がしてきました。図にしてみると、こんな感じです。
EC2が無くなることで運用負荷が激減すると思います。

![スクリーンショット 2018-11-30 17.02.36.png](https://qiita-image-store.s3.amazonaws.com/0/77333/d97c3551-6222-8765-2f47-5e3ca0675164.png)

LambdaのruntimeにRubyやPHPも指定できるようになったようなので、これまでEC2上で動かしていた簡単なプログラムは、Lambdaに移行できるかもしれません。

運用で楽をしたいので、ブログを見て、取り急ぎテストした結果でした。