Chalice を使って AWS Lambda 上に Flask/Bottle のようにWebアプリケーションを構築する

# はじめに

小規模なWebアプリケーションを作成する場合、Pythonでは [Flask](https://flask.palletsprojects.com/en/1.1.x/) や [Bottle](https://bottlepy.org/docs/dev/) を利用できる。
これらのフレームワークは「どのURLに対して」「どのプログラムを動かす」といった対応を Python のデコレータで対応させることで実現できる。
例えば、以下の Flask アプリケーションでは `/` にHTTPアクセスした時に `Hello, World!` を返すWebサーバーを実装しており、ルーティング・レスポンス応答が非常に分かりやすい。

```python:Flaskの公式ページから引用
# https://flask.palletsprojects.com/en/1.1.x/quickstart/
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'
```

さて、これらの Web アプリケーションは開発用途でローカルで動かす場合はすんなりと動いてくれる。 
しかし、本番稼働として別のサーバーで稼働させようとした場合、アクセス可能なサーバーを調達したり、`nginx` + `uwsgi/gunicorn` などのミドルウェアを稼働させる必要があったりと、開発完了から本番稼働までに準備するものが多い印象である。 
特にサーバーに関してはランニングコスト(実費)がかかったり、動作させているミドルウェアがダウンしてサービスが提供できていないことへの対処、ディスクフルなどアプリケーションとは別にちゃんとサーバーが正常に動いているかを考える必要がある、更にはOSやミドルウェアのセキュリティサポート期間切れへの対応などがあったりと、可能であればそもそもサーバーを使いたくない、という考えがある。 
こういったモチベーションから、

- Pythonを使って Flask/Bottle のように簡単な記述でWebアプリケーションが作成できる
- サーバーを管理せずとも上記のWebアプリケーションを運用できる

という2点が解決できないかと考え、結果として AWS Lambda 用のマイクロフレームワーク [Chalice](https://github.com/aws/chalice) を利用することでこれが実現できたので、このやり方を紹介する。


# TL; DR

- Chalice のレスポンスで `text/html` を利用する
- ローカルでのテストには静的ファイルを返すローカル用の route を実装する
    - 色々なコンテンツを返す場合は local に nginx を立ててプロキシする方法も視野に入れる
- HTML form からの POST を受け取れるような設定を Chalice の `@app.route` で行う必要がある
- CloudFront を使って、静的ファイルは S3 Originへ、それ以外は Chalice でデプロイした API Gateway へと流す
- API Gatewayの場合、必ず `https://<FQDN>/<STAGE>/` としてアクセスすることになるが、CloudFrontのOrigin Pathを使うことでトップレベルアクセス (`https://<FQDN>/`) をしても特定ステージの API Gateway の `/` へとアクセス可能になる

# 利用サービスとインフラ構成図

この記事ではAWSで利用可能な多くのリソースを扱う。

- API Gateway + AWS Lambda (Chalice による自動化)
- CloudFront (サイトアクセスの最初に利用するエンドポイント)
- Amazon S3 (image/css/js などの静的ファイルの配置場所)
- Route 53 (独自ドメインの割当て)
- Certificate Manager (CloudFront で利用するSSL証明書の発行・管理)

それぞれのサービスの基本的な設定や使い方などはこの記事では行わない。

これらの関係を図示すると、以下のようになる。

![serverless-webapp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/2fa1f1d0-4473-9096-cc7c-1f7dd950365a.png)


# Webアプリケーションの実装

そこそこコードの量が多くなったので、Gtihub にコード全体をデプロイしている。

https://github.com/t-kigi/chalice-lambda-webserver-example

コードは適時引用するが、全体の流れはこのリポジトリを参照の事。

## 利用したバージョンなど

開発環境は Ubuntu 18.04.

```bash
$ pipenv --version
pipenv, version 2018.11.26
$ pipenv run chalice --version
chalice 1.20.0, python 3.8.2, linux 5.4.0-48-generic
```

## Webアプリケーション開発に必要なChaliceの設定

### 1. レスポンス対応

Chaliceのセットアップなどは[前に記事を書いている](https://qiita.com/t-kigi/items/d2cb619eca6b0d382ab3)のでそちらを参照。 なお、今回は `chalice new-project server` としてプロジェクトを作成している。

Chalice は API 用途で使うことをデフォルトの挙動としているので、特に何も設定しない場合は `application/json` のレスポンスが返ってくる。 しかし、Webアプリケーションとしてブラウザなどで表示可能なページを返す場合、`text/html` のレスポンスを返すとよい。
そのためにはレスポンスヘッダの ContentType を`text/html` に設定してやる必要がある。 上記 Flask の例を Chalice で実現する場合、以下のようなコードになる。

```python:Flaskの例をChaliceで書き直した場合(app.py)
from chalice import Chalice, Response
app = Chalice(app_name='server')

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


### 2. テンプレートエンジンの導入

さて、これだけ設定してやれば body に書かれた内容を `text/html` でクライアント側に返してくれる。
つまり、テンプレートエンジンを使ってページを作成し、その最終結果を body に流してやることでテンプレートエンジンを利用したWebアプリケーションと同じことができる。
ここではテンプレートエンジンとして `jinja2` を利用しているが、もし別のテンプレートエンジンを使いたければそれでも構わない。

今回は以下のディレクトリ構成とした (必要な部分のみ抜粋)

```bash
.
├── app.py             # Chalice のエントリーポイント
└── chalicelib         # deploy するファイルは全部 chalicelib の下に置く必要がある
    ├── __init__.py
    ├── common.py      # 共通項目他を別のモジュールから呼び出すために配置
    ├── template.py    # chalicelib/templates からテンプレートをロードする関数など
    └── templates      # この下にテンプレートを置く
        ├── index.tpl
        └── search.tpl
```

```python:chalicelib/common.py(抜粋)
# 複数ファイルで共有される Chalice オブジェクト
app = Chalice(app_name='server')

# プロジェクトが配置されているディレクトリのパス
chalicelib_dir = os.path.dirname(__file__)
project_dir = os.path.dirname(chalicelib_dir)
```

```python:chalicelib/templates以下のテンプレートファイルを取得できる設定(template.py)
import os
from jinja2 import Environment, FileSystemLoader, select_autoescape
from chalicelib.common import project_dir


template_path = os.path.join(project_dir, 'chalicelib/templates')
loader = FileSystemLoader([template_path])
jinja2_env = Environment(
    loader=loader,
    autoescape=select_autoescape(['html', 'xml']))


def get(template_path):
    ''' テンプレートを取得 '''
    return jinja2_env.get_template(template_path)
```

`app.py` から `chalicelib/templates/index.tpl` を読み込む例は以下の通り。

```python:app.py
from chalicelib import template
from chalicelib.common import app


def html_render(template_path, **params):
    ''' HTMLのレンダリングレスポンスを返す '''
    tpl = template.get(template_path)
    return Response(
        status_code=200,
        headers={'Content-Type': 'text/html'},
        body=tpl.render(**params))


@app.route('/')
def index():
    ''' トップページを返す '''
    return html_render('index.tpl')
```

### 3. ローカルでの検証

ローカル環境での検証には `chalice local` コマンドを使う。
ただ、個人的にはローカル環境とデプロイ後の環境で違う設定をしたいことが多々あるので、ローカル検証用に `local` ステージを作成することをオススメする。 local ステージを作成しておくとと、例えば

- localステージでのみ profile を使って AWS リソースにアクセスする(本番時はIAM Roleを使うのでProfileは利用しない)
- localステージでのみ有効な route を定義する

といったことを実現できる

localステージの作成のために、`.chalice/config.json` の `stages` に `local` スコープを書き加える。

```json
{
  "version": "2.0",
  "app_name": "server",
  "stages": {
    "dev": {
      "api_gateway_stage": "v1"
    },
    "local": {
      "environment_variables": {
        "STAGE": "local"
      }
    }
  }
}
```

これを書き加えた後、`chalice local --stage local` として起動する（既に稼働している場合は一度プロセスを停止して再起動)。 これで `--stage local` として実行した場合にのみ `STAGE` という環境変数が `local` という値で定義されるようになる。


### 4. 静的ファイルのレスポンス対応

開発で利用する主な用途の静的ファイルには、画像、CSS、JavaScriptのコードがある。 そこで、これらを配置するパスを決め、そこへのアクセス時にはそれらのファイルを読み込んで返すこととする。
これらのファイルは最終的にS3にアップロードし、Lambdaのアップロードの中には含めたくないため `chalicelib` の外に配置している。 用意したディレクトリ構造は以下の通り。

```bash:静的ファイルの説明に必要な部分のみ抜粋
.
├── server
│   ├── app.py
│   ├── chalicelib
│   │   ├── __init__.py
│   │   ├── common.py
│   │   └── staticfiles.py
│   └── static -> ../static
└── static
    ├── css
    │   └── style.css
    ├── images
    │   ├── sample.png
    │   └── sub
    │       └── sample.jpg
    └── js
        └── index.js
```

`chalice local --stage local` でサーバーを実行すると `localhost:8000` にサーバーが立つ。 そのため、ここでは `http://localhost:8000/images/sample.png` にアクセスすることで `static/images/sample.png` を取得できるような設定としたい。
これを実現するために `chalicelib/staticfiles.py` を用意した。

```python:chalicelib/staticfiles.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

'''
chalice local で動作する static file を返す実装です。
本番では CloudFront -> S3 へのパスで対処する内容となるため、
開発時にのみ利用することを想定しています。
'''

import os

from chalice import Response
from chalice import NotFoundError
from chalicelib.common import app, project_dir


def static_filepath(directory, file, subdirs=[]):
    ''' ローカルサーバーで static file のパスを生成して返す '''
    pathes = [f for f in ([directory] + subdirs + [file]) if f is not None]
    filepath = os.path.join(*pathes)
    localpath = os.path.join(project_dir, 'static', filepath)
    return (f'/{filepath}', localpath)


def static_content_type(filepath):
    ''' static file 用の Content-Type を返す '''
    (_, suffix) = os.path.splitext(filepath.lower())
    if suffix in ['.png', '.ico']:
        return 'image/png'
    if suffix in ['.jpg', '.jpeg']:
        return 'image/jpeg'
    if suffix in ['.css']:
        return 'text/css'
    if suffix in ['.js']:
        return 'text/javascript'
    return 'application/json'


def load_static(access, filepath, binary=False):
    ''' static file の読み込み '''
    try:
        with open(filepath, 'rb' if binary else 'r') as fp:
            data = fp.read()
        return Response(
            body=data, status_code=200,
            headers={'Content-Type': static_content_type(filepath)})
    except Exception:
        raise NotFoundError(access)


@app.route('/favicon.ico', content_types=["*/*"])
def favicon():
    (access, filepath) = static_filepath(None, 'favicon.ico')
    return load_static(access, filepath, binary=True)


@app.route('/images/{file}', content_types=["*/*"])
@app.route('/images/{dir1}/{file}', content_types=["*/*"])
def images(dir1=None, file=None):
    '''
    ローカル環境用画像ファイルレスポンス
    (Lambdaにデプロイするとパスの都合で動かないのでCloudFrontでS3に流す)
    '''
    (access, filepath) = static_filepath('images', file, [dir1])
    return load_static(access, filepath, binary=True)


@app.route('/css/{file}', content_types=["*/*"])
@app.route('/css/{dir1}/{file}', content_types=["*/*"])
def css(dir1=None, file=None):
    '''
    ローカル環境用CSSファイルレスポンス
    (Lambdaにデプロイするとパスの都合で動かないのでCloudFrontでS3に流す)
    '''
    (access, filepath) = static_filepath('css', file, [dir1])
    return load_static(access, filepath)


@app.route('/js/{file}', content_types=["*/*"])
@app.route('/js/{dir1}/{file}', content_types=["*/*"])
def js(dir1=None, file=None):
    '''
    ローカル環境用JSファイルレスポンス
    (Lambdaにデプロイするとパスの都合で動かないのでCloudFrontでS3に流す)
    '''
    (access, filepath) = static_filepath('js', file, [dir1])
    return load_static(access, filepath)
```

特定パスの時のみ、`static` 以下のファイルを読み込んでそのレスポンスを返す機能を持ったモジュールである。 これを local ステージの時のみ有効にするには、

```python:app.py(抜粋)
import os

stage = os.environ.get('STAGE', 'dev')
if stage == 'local':
    # ローカル時のみ活用
    from chalicelib import staticfiles # noqa
```

として `staticfiles` を読み込んでやれば良い (`# noqa` は `@app.route` に紐付けるためにモジュールをロードしているが、 `app.py` では直接使ってない警告がでるため)。

この状態で `http://localhost:8000` にアクセスして、画像/CSS/JSがちゃんと適用されていれば静的リソースの読み込みは成功である。

```bash
$ pipenv run chalice local --stage local
Serving on http://127.0.0.1:8000
127.0.0.1 - - [23/Sep/2020 17:56:13] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [23/Sep/2020 17:56:14] "GET /css/style.css HTTP/1.1" 200 -
127.0.0.1 - - [23/Sep/2020 17:56:14] "GET /images/sample.png HTTP/1.1" 200 -
127.0.0.1 - - [23/Sep/2020 17:56:14] "GET /js/index.js HTTP/1.1" 200 -

```

`http://localhost:8000` に Chrome でアクセスして表示される画面は以下の通り。 

![chalice-localhost-example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/c3ce9216-d527-8767-3036-80fe87d5b089.png)

これだけあれば簡易的な検証はローカル上で完結可能になる。


#### 参考. nginxでの対処

上記パスを逐一追加していく方法は拡張子が増えたり対象パスが増えていくと、その都度追加していくのは現実的ではない。
そういった場合はローカルに nginx を立てて、特定パスのみ root を `static` とするような location を書いた server 設定を利用すればよい。 その後 http://localhost/ でアクセス・検証を続ければよい。
この記事では nginx の利用方法などは説明しないが、複雑になってくる場合はこちらの導入を検討しても良いため、参考として紹介しておく。

```nginx:nginx.confの具体例
location / {
  # 通常アクセスはこちら
  proxy_pass http://localhost:8000;
}

location ~ ^/(images|css|js)/ {
  # static リソースは固定パスから取得
  root (プロジェクトパス)/static;
}
```

### 5. form からの POST の対応

ブラウザのformからPOSTを行う場合、Content-Type が `application/x-www-form-urlencoded` や `multipart/form-data` として送られる。
Chalice側ではデフォルトだとこれを受け付けないようになっているので、対応メソッドの content_types でこれらを受け取れるようにする必要がある。

```python:chalicelib/common.py
post_content_types = [
    'application/x-www-form-urlencoded',
    'multipart/form-data'
]


def post_params():
    ''' post メソッドに送られたパラメータを dict で返します '''
    def to_s(s):
        try:
            return s.decode()
        except Exception:
            return s
    # str 型に変換して返す
    body = app.current_request.raw_body
    parsed = dict(parse.parse_qsl(body))
    return {to_s(k): to_s(v) for (k, v) in parsed.items()}

```

```python:app.py
@app.route('/search', methods=['POST'],
           content_types=common.post_content_types)
def search():                                                                   
    ''' 検索する '''
    params = common.post_params() # パラメータを dict の形で取得する
```

## Chalice のデプロイと動作確認

Programmic Access が可能な権限の広いIAMを入手しているのであれば、`chalice deploy` コマンド一つでアプリケーションをデプロイ可能となる。
一方、そうではない場合、あるいはCI/CDツールなどを利用する場合は `chalice package` コマンドを使って CloudFormation でデプロイ可能なツールキットを作成すればよい。
具体例は以下の通り。 `--profile` や `--region` などを省略しているので、適時追加すること。

```bash:chalice_packageの利用例
BUCKET=<CloudFormation用のリソースをアップロードするS3バケットを指定>

# CloudFormationでのデプロイ方式に変換
$ pipenv run chalice package build
$ cd build

# Package化してS3にアップロード
$ aws cloudformation package --template-file sam.json \
  --s3-bucket ${BUCKET} --output-template-file chalice-webapp.yml

# CloudFormationでデプロイ
$ aws cloudformation deploy --template-file chalice-webapp.yml --stack-name <STACK名> --capabilities CAPABILITY_IAM
```

今回は `chalice deploy` コマンドでデプロイした結果を使う。 `--stage` を指定していないので、ここでは `dev` ステージの設定が使われる(同様に `--region` , `--profile` は省略)。

```bash
$ pipenv run chalice deploy
Creating deployment package.
Reusing existing deployment package.
Creating IAM role: server-dev
Creating lambda function: server-dev
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:***********:function:server-dev
  - Rest API URL: https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/
```

デプロイしたURLにアクセスすることでちゃんと `text/html` が返ってくる。

```bash
$ curl https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>HELLO</title>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <h1>Lambda Web Hosting</h1>
  <p>バックエンドとして AWS Lambda + Chalice (Python) で Flask/Bottle っぽく動かして見るサンプルです。</p>
  <h2>Load Static Files</h2>
  <p>h1タグに別のファイル /css/style.css から読み取られたスタイルが当たっています。</p>
  <p>画像は以下の通りです。</p>
  <img src="/images/sample.png"/><br>
  <p>JavaScriptも読み込まれています。</p>
  <span id="counter">0</span><br>
  <button id="button" type="button">カウンター (ボタンを押すと数値を加算します)</button>
  <h2>Form Post</h2>
  <p>内部で持っているデータベースモックから一致するものを取得します。</p>
  <form method="POST" action="/search">
    <label>検索キーワード: </label>
    <input type="text" name="keyword" value="" />
    <br>
    <button type="submit">検索</button>
  </form>
  <script src="/js/index.js"></script>
</body>
</html>
```

## 静的ファイルを S3 Bucket にデプロイ

どこかに一つS3バケットを作成し、ここのコンテンツをCloudFrontから参照できるようにする。
今回は `sample-bucket.t-kigi.net` バケットを用意した。
これは例えば `awscli` を使って以下のように一括アップロード・更新ができる。

```bash
# 静的ファイルのrootに移動
cd static
# ファイルを全部コピー
$ aws s3 sync . s3://sample-bucket.t-kigi.net/
upload: css/style.css to s3://sample-bucket.t-kigi.net/css/style.css
upload: js/index.js to s3://sample-bucket.t-kigi.net/js/index.js  
upload: ./favicon.ico to s3://sample-bucket.t-kigi.net/favicon.ico   
upload: images/sub/sample.jpg to s3://sample-bucket.t-kigi.net/images/sub/sample.jpg
upload: images/sample.png to s3://sample-bucket.t-kigi.net/images/sample.png
```

## CloudFront の設定

ウェブサイトのアクセス元としてCloudFrontのDistributionを立てるために以下の設定を行う。 なお、全てManaged Consoleで実施する手順となる。

- Certification Manager で **us-east-1 に** 対象ドメインのSSL証明書を作成する
    - CloudFrontはリージョン指定がないので、利用するリソースは全て us-east-1 に作成する必要がある
    - 今回はRoute53で管理しているホストゾーンのドメインに管理者であることを証明するレコードを追加することで証明書を有効化&自動更新するようにしている
- `Create Distribution` > `Web` を選択してディストリビューションを作成し、以下を設定
    - Origin Domain Name に API Gateway の **FQDN** を入力する (例: `**********.execute-api.ap-northeast-1.amazonaws.com` )
        - 補足: URIを張り付ければここと次のOrigin Pathは適切な値に設定してくれる
    - トップページアクセスのために Origin Path に API Gateway のステージを入れる (例: `/v1` )
    - Viewer Protocol Policyを `Redirect HTTP to HTTPS` とする (API GatewayはHTTPSのみ受け付けるため)
    - Allowed HTTP Methods はWebアプリケーションサーバーとして全てのメソッドを受け入れるために。"GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE" を指定する
    - Cache and origin request settings には Cache Policy として "Managed-CachingDisabled" を指定する (この段階ではCDNをキャッシュサーバーとして使う設定にはしない)
    - Distribution Settings の Alternate Domain Names に事前に準備したドメインのFQDN (ここでは `lambdasite.t-kigi.net` ) を指定
    - SSL Certificate に事前に作成した SSL 証明書を指定 
        - 証明書を作成した直後だと選択肢に出てこないことがあるので、その場合はしばらく待つ
        - それでも出てこない場合は作成したリージョンが us-east-1 であるかをもう一度チェック
    - Default Root Object には **何も記入しない**
        - ここに書いてるとデフォルトアクセス時に関係ないURLのリクエストをAPI Gatewayに流してしまいトップページへのアクセスに失敗してしまう
    - それ以外の項目はデフォルト or 自分の都合の良いように決定
- Distribution が作成された後、IDを選択して `Origins and Origin Groups` タブを選択
    - Create Origin で事前に準備した静的ファイル用のS3バケットを追加
        - Origin Path は未入力のまま (バケットのトップ = URLのトップとする)
        - Restrict Bucket Access を Yes とする
        - Create a New Identity を選択してCDNからS3にアクセスする新しい権限を作成する
        - Grant Read Permissions on Bucket で "Yes, Update Bucket Policy" でバケットポリシーを更新する
- `Behaviors` タブを開き、Create Behavior で特定パスのオリジンを指定する
    - `/images/*` , `/css/*` , `/js/*` , `/favicon.ico` へのアクセスをS3 Originへと向ける (下記画像参照)
    - Allowed HTTP Methods は GET/HEAD で良い (あるいは適切に設定)

![cloudfront-s3-behaviors.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/887ea7be-91ed-1195-96a9-6a0e91ef6012.png)

以上の手順を行い、CloudFront の状態が Deployed になるまで待つ。 大体10～20分程度かかる。


## サンプルサイト

以上のデプロイを行ったサンプルサイトは以下の通り。

https://lambdasite.t-kigi.net

この手順で CloudFront の `/` は API Gateway の `/v1/` へと転送されるので、サイトトップレベルのアクセスを捌けるようになる。
なお、`/v1` -> `/v1` へとそのまま転送したい場合はオリジン設定の `Origin Path` に何も入力しなければよい。
 
デフォルトのLambdaの同時実行数は1000なので、CDNのキャッシュと合わせれば、かなりの同時アクセスを捌けると想定できる。


## お値段比較

> NOTE: 各料金は2020/09/23の記事執筆時点で ap-northeast-1 (東京) リージョンのものを参考にした

AWSで(1年)無料枠とされている t2.micro を立てっぱなしにした場合の30日(一カ月)の料金は

- インスタンス稼働代金: 10.944USD = `(0.0152USD/hour * 24 hours * 30 days)`
- Amazon Linux2 AMI に付随する最小の EBS Volume (8GB) 代金: 0.96USD = `(0.12USD/month * 8)`
- その他、AWS外へのデータ転送量 (0.114USD/GB)

となり、小規模なシステムであればおよそ1200～1300円程度となる (記事執筆時現在)。
AWSアカウントを作成してから12カ月のうちは無料となるが、これを超えた場合、あるいは1企業内で複数アカウントを作成して紐づけた場合は無料枠は消失してしまう(経験談)。
ただし、これは冗長性を考慮しないシステムであり、インスタンスがダウンした場合は手動で対処することが前提である。
冗長化させようと思った場合、サーバーを2台(以上)用意したり、ALBを前段に置く (月額+約20USD) 必要があるなど、地味にコスト面での問題が出てくる。

一方、AWS Lambda を使う場合であれば、

- 1 か月ごとに 100 万件の無料リクエスト、および 40 万 GB-秒のコンピューティング時間は(アカウントを作ってからの期限に関係なく)無料
    - アプリケーションを128MBのメモリで動かすのであれば、約37日 `(40万GB-秒÷0.125GB=320万秒)` 分の累計Lambda稼働時間が1か月あたり無料
    - 100万件以上のリクエストがあっても、次の100万件あたり 0.2 USD
- S3の料金は 100万リクエスト(GET)で 0.37USD、1GB保持あたり 0.025USD/month という低料金
- データ転送量はCloudFrontを通しても同じ (AWS外へのデータ転送量 (0.114USD/GB))
- それ以外にもAPI Gateway, Route 53など、細々した料金は発生する

といった形で、アクセス頻度の少ないサーバーであれば 1～2USDぐらいもあれば十分運用することが可能になるだろう。
単純比較でも大きな利点があるが、更に「ここで利用するAWSの各サービスは冗長化されている」というのも特筆すべき事項である。
秒間数万リクエストといったデータを全てLambdaで捌くのは厳しいかもしれないが、CloudFrontのキャッシュとAWS Lambdaの並列実行性を利用すれば、秒間数百リクエスト程度のサービスなら特に工夫せずとも十分捌ききれる可能性はある。
欠点を挙げるとすれば、Lambdaの仕組みとしてアクセスがない場合の初回立ち上がりにやや時間がかかることもあり、レスポンス時間が安定しないことがあるかもしれない。

## 完全な静的サイトやSPAの場合

単なる静的コンテンツのみで構成されるウェブサイトであれば CloudFront - S3 だけで実現できる。 
さらに言えばS3に各種静的コンテンツを置いて、API GatewayでAPIを提供し、CloudFrontで一部パスを直接API GatewayにつなげてCORSを気にしないようなSPA (Single Page Application) にするというのがよりモダンな使い方ではあると思う。

ただ、今回はバックエンドでやり取りしたデータを使ってテンプレートエンジンを動かしたいという要件があったため AWS Lambda をデフォルトオリジンとしてそっちに通信を流すような設計を考えたという経緯がある。


# まとめ

Chalice + AWS Lambda + AWSの各種リソースを利用して Serverless だが Flask/Bottle のようにWebアプリケーションを実装し、実際の運用環境に乗せる方法を紹介した。
ただ、結果的に様々なAWSのサービスを利用することで1つのサービスを構築するため、特に知識がない状態でこの記事を見ても理解が難しいものになってしまったとは思う。

しかし、サーバーを管理しないWebアプリケーションを作る、という意味では「最終的にはコーディングに集中できる環境を作っている」とも言える。  サーバーを管理しないためにサーバーや各インフラのことを理解するというのも遠回りに感じるかもしれないが、この時代であれば学んで損はないと個人的には考えている。


## 記事の範囲外の発展内容

ここで紹介したプロジェクトをベースに

- アプリケーション側でデータストアとしてDynamoDBを利用する
    - RDBを使うにはそれなりに制約があるので、Lambda - DynamoDB の組での実装を覚える教材に良いのではないか
- CloudFront の利用による効率的キャッシュの運用
    - 今回はAPIのキャッシュはOFF、静的リソースのキャッシュはONにしている
    - CDNのキャッシュはサイトの作りや設定次第で他利用者の情報流出の原因になったりするので、本番で利用する場合はパスとキャッシュの関係をよくよく考え、テストして使う事
- 認証機構の仕組みの導入
    - 今はAPI GatewayのURIに直接アクセスしてもレスポンスが返ってくる
    - API Gateway に API Key を導入したり、 Lambdaのヘッダで X-Forwarded-For による接続元IPアドレスを判定することで接続制限を実装できる
    - [CloudFront の 署名付きCookie](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html)を使うことでより制限のあるサイトを構築できる
    - IDaaS が別にあるなら Chalice の Authorizer の仕組みを使ってもよい
- Python以外の言語でWebアプリケーションを実装する
    - Chaliceのサポート部分を自分の好きな言語・フレームワークで書きかえればそれだけで同じことができる

などの点を導入していくことで、より実際の運用に耐えうる環境を構築することができる。