Lambda(Python) や API Gateway の管理を Chalice でやってみた

Chalice を使う機会があったので、
せっかくなので備忘録も兼ねてまとめました。
ちなみに Python 歴は1ヶ月未満です。

## Chalice とは？

https://github.com/aws/chalice
Amazon 産の AWS 向け Python フレームワークです。
AWS Lambda を使用する際に簡単に管理が出来るようになります。

また API Gateway, S3, SNS, SQS, CloudWatch Event などといった連携サービスもそのまま一緒に管理できます。

Chalice では、
Python の Chalice モジュールを利用しつつ、
コマンドラインツールで chalice コマンドを叩いてデプロイ等をする形になります。

また `chalice delete` コマンドを用いて、簡単に削除もできます。

## 開発環境

### 前提

- AWS Credentials が設定されていること
- Python 3.6 系が使えること

### 開発環境のバージョン確認

```bash
$ python --version
Python 3.6.6

$ pip --version
pip 10.0.1

$ chalice --version
chalice 1.6.0
```

## インストール方法

```bash
$ pip install chalice
```

## Hello World!

https://github.com/aws/chalice#quickstart
後述する `@app.route` を使って、
API Gateway + Lambda のデプロイが実施されます。

```bash
$ chalice new-project chibi-chalice

$ tree -a chibi-chalice/
chibi-chalice/
├── .chalice
│   └── config.json
├── .gitignore
├── app.py
└── requirements.txt

$ cd chibi-chalice

$ chalice deploy
Creating deployment package.
Creating IAM role: chibi-chalice-dev
Creating lambda function: chibi-chalice-dev
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-chalice-dev
  - Rest API URL: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/

# このように `@app.route` を実装して chalice deploy を叩くと
# IAM Role が自動的に作成され、
# Lambda Function が自動的に作成され、
# API Gateway が自動的に作成されます。
```

```bash:動作確認
# もう API が叩ける状態に！
$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/
{"hello": "world"}
```

### Local でも実行できる！

```bash
$ chalice local
Serving on 127.0.0.1:8000
```

```bash
$ curl localhost:8000
{"hello": "world"}
```

## 各種使用方法

Hello World を試したところで、
自動的に API Gateway + AWS Lambda が簡単にデプロイできることがわかりました。

Chalice は Decorator を使って各種サービス向けの実装ができるため、
掘り下げて色々デプロイして遊んでみます。

### @app.route

https://github.com/aws/chalice/blob/master/docs/source/topics/routing.rst

`@app.route` は、
Hello World でも使用した「API Gateway + AWS Lambda」の定義になります。

#### デコレーション定義

```py
def route(self, path, **kwargs):
```

#### サンプル

```py
from chalice import Chalice

app = Chalice(app_name='chibi-chalice')


@app.route('/')
def index():
    return {'hello': 'world'}
```
このような形で実装してデプロイを行えば、
Hello World のように GET API が叩けるわけです。

#### POST や PUT を実装する場合

```py
@app.route('/', methods=['POST'])
def index():
    request = app.current_request
    return {
        'method': request.method,
        'json-body': request.json_body
    }
```

```bash:動作確認
$ curl -X POST -H "Content-Type:application/json" -d '{"Hello":"World"}' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/
{"method": "POST", "json-body": {"Hello": "World"}}
```

#### URL にパラメータを使う場合

```py
@app.route('/users/{user}')
def index(user):
    return {'user': user}
```

```bash:動作確認
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/users/chibi
{"user": "chibi"}
```

#### URL クエリを使う場合

```py
@app.route('/users/{user}')
def index(user):
    request = app.current_request
    return {
        'user': user,
        'query': request.query_params
    }
```

```bash:動作確認
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/users/chibi?hoge=foo
{"user": "chibi", "query": {"hoge": "foo"}}
```

#### レスポンスをカスタマイズする

```py
from chalice import Chalice, Response

app = Chalice(app_name='chibi-chalice')


@app.route('/')
def index():
    return Response(body='Hello World!',
                    status_code=200,
                    headers={'Content-Type': 'text/plain', 'hoge': 'foo'})
```

```bash:動作確認
$ curl -v https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 12
< hoge: foo
Hello World!
```

#### CORS サポート

```py
# このように書けば良いらしい
@app.route('/', methods=['PUT'], cors=True)
```

### @app.lambda_function

https://github.com/aws/chalice/blob/master/docs/source/topics/purelambda.rst

`@app.lambda_function` は、
純粋な「AWS Lambda」の定義になります。

#### デコレーション定義

```py
def lambda_function(self, name=None):
```

#### サンプル

```py
from chalice import Chalice

app = Chalice(app_name='chibi-chalice')


@app.lambda_function()
def handler(event, context):
    return {'hello': 'lambda'}
```

```bash:デプロイ
$ chalice deploy
Creating deployment package.
Creating IAM role: chibi-chalice-dev
Creating lambda function: chibi-chalice-dev-handler
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxx:function:chibi-chalice-dev-handler

# このように `@app.lambda_function` を実装して chalice deploy を叩くと
# IAM Role、Lambda Function が自動的に作成されます。
# ※ API-Gateway は作成されません。
```

```bash:動作確認
$ aws lambda invoke --function-name chibi-chalice-dev-handler outputfile.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

$ cat outputfile.txt 
{"hello": "lambda"}
```

#### Lambda 関数名を任意に指定する場合

```py
@app.lambda_function(name='MyFunction')
def handler(event, context):
    return {'hello': 'MyFunction'}
```

```bash:動作確認
$ aws lambda invoke --function-name chibi-chalice-dev-MyFunction outputfile.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

$ cat outputfile.txt 
{"hello": "MyFunction"}
```

### @app.schedule

https://github.com/aws/chalice/blob/master/docs/source/topics/events.rst#scheduled-events

`@app.schedule` は、
「CloudWatch Event + AWS Lambda」の定義になります。

#### デコレーション定義

```py
def schedule(self, expression, name=None):
```

#### サンプル

```py
from chalice import Chalice, Rate

app = Chalice(app_name="chibi-chalice")


@app.schedule(Rate(1, unit=Rate.MINUTES))
def handle_schedule(event):
    print(event)
    return {"hello": "world"}
```

```bash:デプロイ
$ chalice deploy
Creating deployment package.
Creating IAM role: chibi-chalice-dev
Creating lambda function: chibi-chalice-dev-handle_schedule
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-chalice-dev-handle_schedule
# このように `@app.schedule` を実装して chalice deploy を叩くと
# IAM Role、Lambda Function が自動的に作成されます。
# ※スケジュールイベントはきっと設定されているはず。。。
```

```bash:動作確認
$ aws events list-rules --name-prefix chibi-chalice-dev-handle_schedule
{
    "Rules": [
        {
            "Name": "chibi-chalice-dev-handle_schedule-event",
            "Arn": "arn:aws:events:ap-northeast-1:xxxxxxxxxx:rule/chibi-chalice-dev-handle_schedule-event",
            "State": "ENABLED",
            "ScheduleExpression": "rate(1 minute)"
        }
    ]
}
```

#### 別の記述方法

```py
@app.schedule('rate(1 minutes)')
def handle_schedule():
    print(event)
    return {'hello': 'schedule'}
```

### @app.on_s3_event

https://github.com/aws/chalice/blob/master/docs/source/topics/events.rst#s3-events

`@app.on_s3_event` は、
「S3 + AWS Lambda」の定義になります。

#### デコレーション定義

```py
def on_s3_event(self, bucket, events=None, prefix=None, suffix=None, name=None):
```

#### サンプル

```py
from chalice import Chalice

app = Chalice(app_name='chibi-chalice')


@app.on_s3_event(bucket='chibi-existing-bucket')
def handle_s3_event(event):
    print(f'Object uploaded for bucket: {event.bucket}, key: {event.key}')
```

※ Bucket は存在している必要があります。

```bash:デプロイ
$ chalice deploy
Creating deployment package.
Creating IAM role: chibi-chalice-dev
Creating lambda function: chibi-chalice-dev-handle_s3_event
Configuring S3 events in bucket chibi-existing-bucket to function chibi-chalice-dev-handle_s3_event
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-chalice-dev-handle_s3_event
# このように `@app.on_s3_event` を実装して chalice deploy を叩くと
# IAM Role、Lambda Function が自動的に作成され、
# S3 events を設定してくれます。
```

```bash:動作確認
$ aws s3api get-bucket-notification-configuration --bucket chibi-existing-bucket
{
    "LambdaFunctionConfigurations": [
        {
            "Id": "xxxxxxxxxx",
            "LambdaFunctionArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-chalice-dev-handle_s3_event",
            "Events": [
                "s3:ObjectCreated:*"
            ]
        }
    ]
}

```

### @app.on_sqs_message

https://github.com/aws/chalice/blob/master/docs/source/topics/events.rst#sqs-events

`@app.on_sqs_message` は、
「SQS + AWS Lambda」の定義になります。

#### デコレーション定義

```py
def on_sqs_message(self, queue, batch_size=1, name=None):
```

#### サンプル

```py
from chalice import Chalice

app = Chalice(app_name='chibi-chalice')


@app.on_sqs_message(queue='chibi-existing-queue')
def handle_sqs_message(event):
    for record in event:
        print(f'Message body: {record.body}')
```

※Queue は存在している必要があります。
※Queue の「デフォルトの可視性タイムアウト」は Lambda のタイムアウト以上に設定する必要があります。

```bash:デプロイ
$ chalice deploy
Creating deployment package.
Creating IAM role: chibi-chalice-dev
Creating lambda function: chibi-chalice-dev-handle_sqs_message
Subscribing chibi-chalice-dev-handle_sqs_message to SQS queue chibi-existing-queue
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-chalice-dev-handle_sqs_message
# このように `@app.on_sqs_message` を実装して chalice deploy を叩くと
# IAM Role、Lambda Function が自動的に作成され、
# SQS の Lambda トリガーを設定してくれます。
```

```bash:動作確認
$ aws lambda list-event-source-mappings --function-name chibi-chalice-dev-handle_sqs_message
{
    "EventSourceMappings": [
        {
            "UUID": "xxxxxxxxxx",
            "BatchSize": 1,
            "EventSourceArn": "arn:aws:sqs:ap-northeast-1:xxxxxxxxxx:chibi-existing-queue",
            "FunctionArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-chalice-dev-handle_sqs_message",
            "LastModified": 1537413283.145,
            "State": "Enabled",
            "StateTransitionReason": "USER_INITIATED"
        }
    ]
}
```

### @app.on_sns_message

https://github.com/aws/chalice/blob/master/docs/source/topics/events.rst#sns-events

`@app.on_sns_message` は、
「SNS + AWS Lambda」の定義になります。

#### デコレーション定義

```py
def on_sns_message(self, topic, name=None):
```

#### サンプル

```py
from chalice import Chalice

app = Chalice(app_name='chibi-chalice')


@app.on_sns_message(topic='chibi-existing-topic')
def handle_sns_message(event):
    print(f'Received message with subject: {event.subject}, message: {event.message}')
```

※Topic は存在している必要があります。

```bash:デプロイ
$ chalice deploy
Creating deployment package.
Creating IAM role: chibi-chalice-dev
Creating lambda function: chibi-chalice-dev-handle_sns_message
Subscribing chibi-chalice-dev-handle_sns_message to SNS topic chibi-existing-topic
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-chalice-dev-handle_sns_message
# このように `@app.on_sns_message` を実装して chalice deploy を叩くと
# IAM Role、Lambda Function が自動的に作成され、
# SNS のサブスクリプションに Lambda を設定してくれます。
```

```bash:動作確認
$ aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxx:chibi-existing-topic
{
    "Subscriptions": [
        {
            "SubscriptionArn": "arn:aws:sns:ap-northeast-1:xxxxxxxxxx:chibi-existing-topic:xxxxxxxxxx",
            "Owner": "xxxxxxxxxx",
            "Protocol": "lambda",
            "Endpoint": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-chalice-dev-handle_sns_message",
            "TopicArn": "arn:aws:sns:ap-northeast-1:xxxxxxxxxx:chibi-existing-topic"
        }
    ]
}
```

### @app.authorizer

https://github.com/aws/chalice/blob/master/docs/source/topics/authorizers.rst#built-in-authorizers

`@app.authorizer` は、
「API Gateway の Lambda オーソライザー + Lambda」の定義になります。
Lambda のデプロイとオーソライザーの設定が一括でできます。

#### デコレーション定義

```py
def authorizer(self, name=None, **kwargs):
```

#### サンプル

```py
from chalice import Chalice, AuthResponse

app = Chalice(app_name='chibi-auth')


@app.authorizer()
def demo_auth(auth_request):
    token = auth_request.token
    # This is just for demo purposes as shown in the API Gateway docs.
    # Normally you'd call an oauth provider, validate the
    # jwt token, etc.
    # In this exampe, the token is treated as the status for demo
    # purposes.
    if token == 'allow':
        return AuthResponse(routes=['/', '/one', '/two'], principal_id='user')
    else:
        # By specifying an empty list of routes,
        # we're saying this user is not authorized
        # for any URLs, which will result in an
        # Unauthorized response.
        return AuthResponse(routes=[], principal_id='user')


@app.route('/', authorizer=demo_auth)
def index():
    return { 'message': 'root' }

@app.route('/one', authorizer=demo_auth)
def one():
    return { 'message': 'one' }

@app.route('/two', authorizer=demo_auth)
def two():
    return { 'message': 'two' }
```

※サンプル通りのコードなのだが何故か動かないので `/one` , `/two` も追加
※ルート( `/` )は上手く動かせないのか。。。？
※また、コメントに記載されている通り本来であれば jwt token 等を validate します。

```bash:デプロイ
$ chalice deploy
Creating deployment package.
Creating IAM role: chibi-auth-dev
Creating lambda function: chibi-auth-dev
Creating lambda function: chibi-auth-dev-demo_auth
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-auth-dev
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:chibi-auth-dev-demo_auth
  - Rest API URL: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/
# このように `@app.authorizer` を実装して chalice deploy を叩くと
# IAM Role、Lambda Function, API Gateway が自動的に作成され、
# API Gateway に Lambda Authorizer が設定されます。
```

```bash:動作確認
$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api
{"message":"Unauthorized"}
$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api -H "Authorization: allow"
{"Message":"User is not authorized to access this resource"}

$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/one
{"message":"Unauthorized"}
$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/one -H "Authorization: allow"
{"message": "one"}

$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/two
{"message":"Unauthorized"}
$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/two -H "Authorization: allow"
{"message": "two"}
```

### @app.route (オーソライザ)

@app.authorizer を使わなくても簡単に設定が可能です。

#### IAM オーソライザ

https://github.com/aws/chalice/blob/master/docs/source/topics/authorizers.rst#aws-iam-authorizer

```py
from chalice import Chalice, IAMAuthorizer

app = Chalice(app_name='chibi-auth')

authorizer = IAMAuthorizer()


@app.route('/iam-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
    return {"success": True}
```

#### Cognito User Pool オーソライザ

```py
from chalice import Chalice, CognitoUserPoolAuthorizer

app = Chalice(app_name='chibi-auth')

authorizer = CognitoUserPoolAuthorizer(
    'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name'])


@app.route('/user-pools', methods=['GET'], authorizer=authorizer)
def authenticated():
    return {"success": True}
```

#### Lambda オーソライザ

```py
from chalice import Chalice, CustomAuthorizer

app = Chalice(app_name='chibi-auth')

authorizer = CustomAuthorizer(
    'MyCustomAuth', header='Authorization',
    authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-01'
                    '/functions/arn:aws:lambda:region:account-id:'
                    'function:FunctionName/invocations'))


@app.route('/custom-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
    return {"success": True}
```

## まとめ

初めてレベルで Python を触ったけど Chalice 便利！
また何か新しいものを触ったら記事にしたい。