[構築録] AWS サーバレス構成で API を作ってみたのですが

初投稿です。

本記事では、**AWS でサーバレス構成で API を作成した際の構築録** をまとめます。~~何番煎じd~~

夏休みの自由研究がまだ決まっていない方、「APIを作ってみたい！」な方のご参考になれば幸いです。

この記事を書く前に「全部一通り一気通貫で教えてくれる記事ないかな…」と探してみたところ、あんまり一気通貫で書いている例は多くないので「 **一気通貫で構築できる記事を！** 」と、意気込んで書いてしまった結果、**記事が長い** です。ごめんなさい。

# TL; DR;

- AWS のサービスを組み合わせることで、いわゆるサーバレスAPサーバを短時間(1時間程度)で作成可能
- が、設定項目が多いためそれなりに勉強が必要。初回は勉強を兼ねながらなので、いっぱい時間がかかりました
- API設計を先に検討しておかないとサービス間の結合でうまくいかなくなるので注意
- ハマっても泣かない
- AWSさん、高機能で助かるんですがもう少し初心者に優しくしてくださいお願いしま（略）

# 前置き

## 構築の背景

「あ、そうだ。APIを作ってみたい」という突発的な思いつきでAPIを作成してみたくなりました。
さらに、「どうせなら、業務ではまず触らないサーバレスで構築してみたいなぁ」と思ってしまったわけです。

少し作成手順をググってみると、 AWS 一色 (Route53, CloudFront, API Gateway, Lambda, S3) の構成での構築例があるので、それに乗っかれば「これは楽勝だな」と考えていた次第です。

だが、それが甘かった。

## 構築した構成

今回、以下のような、単純な構成で API (兼Webサイト) を作成してみたかったのです。

![sample_API_SystemArch.png](https://qiita-image-store.s3.amazonaws.com/0/280792/9ce874ac-0926-925a-7957-37c71bac98d2.png)

``https://{host}/`` にアクセスするとWebページが表示されて、
``https://{host}/api/`` 以下にアクセスするとAPIリクエストとして処理されるイメージです。
Webページ側には、このAPIについての説明ドキュメントなどを提供することを想定しています。

※ 図では ``http`` と書いちゃっていますが、``http`` リクエストは ``https`` にリダイレクトするイメージです。

# 構築ログ

## 0. 構築する順番

以下のように、**お外から遠いところから** 順番で作成するのがオススメです（設定時に ARN などを参照するため)

1. S3
2. Lambda
3. API Gateway
4. CloudFront
5. Route53
6. (必要に応じて）Certificate Manager (ACM)
    - 作成した証明書を適用するため CloudFront に設定追記します

## 1. S3

静的コンテンツ用とマスタデータ用のバケットを2つ作成しました。

### 1.1. [バケット#1] マスタデータ管理バケット (master-bucket)

以下のような S3 キーの所在にマスタデータを用意しました。マスタデータの中身はJSONで記載されているものとします（御都合主義）。

``master-bucket/master/master-data.txt``

他、特別な設定はしていません。公開設定、バケットポリシはデフォルトのままにしました

### 1.2. [バケット#2] 静的ファイル用バケット (assets-bucket)

以下のようなS3キーの所在に静的コンテンツ (index.htmlやCSSや画像など) を配置しました

- ``assets-bucket/prd/index.html``
- ``assets-bucket/prd/assets/css/``
- ``assets-bucket/prd/assets/images/``

こちらのバケットでは、CloudFront からのアクセスのみを許可するため、バケットポリシを設定します。
ですが、後述の CloudFront での S3 用オリジン設定後に以下のように勝手にバケットポリシを設定してくれます。なので、後で確認するだけで OK でした

```json:静的ファイル用バケットのバケットポリシ
{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity XXXXXXXXXXXXX"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::assets-bucket/prd/*"
        }
    ]
}
```

## 2. Lambda

S3にあるマスタデータファイルを読み出してその情報を return する Lambda 関数を作成しました。
作成手順はググればたくさん出てくるので割愛します。

一応の参考として、作成したコードと、周辺設定を以下にメモします。

- ランタイム: Python 2.7
- 実行ロール: ``AmazonS3ReadOnlyAccess`` ポリシをアタッチした Lambda 実行ロールを設定
- メモリ: 128MB
- タイムアウト: 3秒
- 関数コード: 以下

```python:S3からファイルを読みだす関数サンプル

import boto3

s3 = boto3.client('s3')

def lambda_handler(event, context):
    last_modified, raw_data = get_master_data_from_s3()

    func_result = {
        'last_modified': last_modified,
        'data': raw_data
    }

    return func_result


def get_master_data_from_s3():

    AWS_S3_BUCKET_NAME = 'master-bucket'
    KEY_NAME = 'master/master-data.txt'
    object = s3.get_object(Bucket=AWS_S3_BUCKET_NAME, Key=KEY_NAME)

    return [object.get('LastModified').strftime('%Y-%m-%d'), object.get('Body').read().decode('utf-8')]
```

※ API では JSON 文字列でレスポンスすることを想定しているため、return するオブジェクトに json.dumps() したくなりますが、不要です

作成後、Lambda の画面でテストを行い、単独で動作することを確認します。

## 3. API Gateway

基本的な作成手順は [こちら](https://dev.classmethod.jp/cloud/aws/getting-start-api-gateway/) を参考に作成しました。
今回の構成で大事となるポイントに焦点を当てつつ、構築した構成を以下にまとめます

### 3.1. API の作成

**新しいAPI** にチェックを入れて作成します。今回は CloudFront からのアクセスとなるので、 **エンドポイントタイプ** は **エッジ最適化** を選択しました。

### 3.2. API リソースの作成

サンプルとして、以下のような API リソースを作成しました

``/api/master/``

必要に応じて **API Gateway CORS を有効にする** にチェックを入れます。
なお、リソース作成時に余計に ``OPTIONS`` メソッドが作成されます。今回は不要であったので削除しました。

### 3.3. API メソッドの作成

``GET`` メソッドを作成し、各種設定を行いました。

まず、**メソッドリクエスト** を選択し、以下の項目の設定を変更しました。
API キーは設定しておきたかったので、``true`` にしています。
また、APIにクエリパラメータを定義し、そのパラメータを Lambda に渡したかったので、その設定も行いました。

- **API キーの必要性**: ``true``
- **URL クエリ文字列パラメータ**: ``mode`` を追加


続いて、**統合リクエスト** を選択し、以下の設定を行いました。

- **統合タイプ**: **Lambda 関数**
- **Lambda リージョン**: 先ほど作成した Lambda 関数のリージョンを指定
- **Lambda 関数**: 先ほど作成した Lambda 関数
- **マッピングテンプレート**:
    - **リクエスト本文のパススルー**: **テンプレートが定義されていない場合 (推奨)**
    - **Content-Type**: ``application/json`` を追加し、以下のマッピングテンプレートを記載

```json:マッピングテンプレートサンプル
{
  "mode": "$input.params('mode')"
}
```

※ この「統合リクエスト」では、クエリ文字列パラメータは設定しなくて大丈夫でした。

ここまで設定ができたら、 **メソッドの実行** に戻り、 **テスト** からリクエストテストを行ってみます。問題ないことを確認したら次に進みます


### 3.4. API のデプロイ

リソースの選択画面にて、 ``アクション`` > ``APIのデプロイ`` を選択し、APIをデプロイします。
**デプロイされるステージ** には、自身で管理する上でのステージIDやステージ番号などを設定して新規作成します。

### 3.5. API キー（と使用料プラン）の作成

実験的に API キーを設定してみました。併せて使用料プランも設定の必要がありましたので、併せて設定ログをメモしておきます。

1. 使用料プランの作成
    - プランの名前はなんでも良い
    - **スロットリング** は、短期時間あたりの API アクセス数の上限を設定します。ここでは以下のようにしてみました (超天井低い)
        - **レート**: **1** リクエスト/秒
        - **バースト**: **1** リクエスト数
    - **クォータ** は、長期 (1ヶ月) あたりの API アクセス数の上限を設定します。ここで (以下略)
        - **クォータ**: **100,000** リクエスト数/月
    - **関連つけられたAPIステージ** では、先ほどデプロイしたAPIステージを設定する
2. API キーの作成
    - 作成後に、上記の作成した使用料プランを割り当てて設定


## 4. CloudFront

[こちら](https://qiita.com/horsewin/items/3209083d9fb3a8441895) を参考に作成していきました。

最初に ``Distribution`` を作成し、続いて静的コンテンツ用とAPI用の2種類のオリジン設定を行なっていきました。

設定項目が多すぎるため、スクリーンショットでサンプル設定ログを載せます。
大事なところだけ文章で補足します。

### Distribution 作成

* **Price Class**: 使用する Cloud Front エッジロケーションを制限することでお安くできます。私はお金がないので一番安いものにしました
* **Alternate Domain Names**: 後ほど取得する独自ドメイン名を記載しました
* **SSL Certificate**: 初めは **Default CloudFront Certificate** を使用して乗っかっておきました。独自ドメインを取得した後はSSL証明書を作成し、 **Custom SSL Certificate** に切り替えました
* **Default Route Object**: ``https://{host}/`` にアクセスした際に表示するオブジェクトを指定します。ここでは静的ファイル用 S3 バケットに格納している ``index.html`` を指定しました。
* **Logging**: 今回はあえてやっていませんが、本番運用などを考える際には設定したほうが良いと思います。また、構築時のデバッグのためにも設定した方が問題解決につながると思われます。

![CloudFront設定例_その1.png](https://qiita-image-store.s3.amazonaws.com/0/280792/2f21a02d-fe76-53e8-93dc-424876b85504.png)
![CloudFront設定例_その2.png](https://qiita-image-store.s3.amazonaws.com/0/280792/611f4af8-4f98-0bbe-e5fc-5f267701f765.png)


### 静的コンテンツ用オリジンの作成

* **Origin Domain Name**: S3バケット名を選択します (``assets-bucket.s3.amazonaws.com``)
* **Origin Path**: 参照可能とするS3バケットのキーを指定します (``/prd``)
* **Restrict Bucket Access**: **Yes**
* **Origin Access Identify**: **Create a New Identify** を選択し、IAMロールを作成しました。（スクリーンショットでは作成済みになっているので注意）
* **Grant Read Permissions on Bucket**: **Yes, ...** を選択します（スクリーンショットでは No になっているので注意）

![CloudFront設定例_その3.png](https://qiita-image-store.s3.amazonaws.com/0/280792/3d25b243-fde1-bcb0-3522-e56945a834fb.png)



### 静的コンテンツ用ビヘイビアの設定

* **Path Pattern**: 何も記載せず、 Default としました
* **Viewer Protocol Policy**: HTTP アクセスを HTTPS にリダイレクトさせたかったので **Redirect HTTP to HTTPS** に選択しました (スクリーンショットでは HTTP and HTTPS になっていますので注意)

![CloudFront設定例_その4.png](https://qiita-image-store.s3.amazonaws.com/0/280792/16fc5a96-1a4a-773d-bfdc-23c998aa5134.png)
![CloudFront設定例_その5.png](https://qiita-image-store.s3.amazonaws.com/0/280792/c0583f5f-8a44-2481-1137-4170b6256578.png)



### API オリジンの作成

* **Origin Domain Name**: API Gateway にてデプロイした API ホストを選択します 
* **Origin Path**: APIのデプロイステージ名を選択します

![CloudFront設定例_その6.png](https://qiita-image-store.s3.amazonaws.com/0/280792/7613769b-cc69-8f37-7eb6-668d2b82fc74.png)


### API ビヘイビアの設定

* **Path Pattern**: ``/api/*`` を設定
    * この ``/api/`` は、API Gateway 側で作成した ``/api/master`` というリソースの ``/api/`` の部分に対応します
    * 後々、 API Gateway 側で ``/api/何某`` のようにリソースを追加していく際に、CloudFront 側にて変更が不要となるように、このようにワイルドカード指定 (``/*``) しています。
* **Cache Based on Selected Request Headers**: **whitelist** を選択
* **Whitelist Headers**: 以下の2ヘッダを許可するようにしました
    * **Origin**
    * **X-API-KEY** (前述のAPI キーのリクエストヘッダです)
* **Query String Forwarding and Caching**: **Forward all, cache based on all** を選択しました
    * **※クエリパラメータを使用する場合には、ここで None は選択しないようにしてください**

![CloudFront設定例_その7.png](https://qiita-image-store.s3.amazonaws.com/0/280792/2e0ab1fc-9ec1-4590-d423-40c0f30befc9.png)
![CloudFront設定例_その8.png](https://qiita-image-store.s3.amazonaws.com/0/280792/daa1fbf6-85ae-9efb-8a4b-8e59820870aa.png)
![CloudFront設定例_その9.png](https://qiita-image-store.s3.amazonaws.com/0/280792/e10d5bc1-590e-07ce-572d-ea8770b38313.png)



## 5. Route 53

思いつきで行動を始めたためドメインなど持っているわけがありません。独自ドメインの取得からスタートしました。

### 5.1. 独自ドメインの取得

[こちら](https://qiita.com/schabibi/items/bb2e6bd4e5a1f29eeb90)を参考に取得しました (手順1の部分です)

作成したい API のドメイン名とお財布を相談してから決めます (``.jp`` 高いですね)。
取得完了後、自動的に ``Hosted Zones`` 一覧に登録されました。

### 5.2. レコードセット作成

取得したドメインにについての ``Record Set`` 設定を行います。 ``Hosted Zones`` から自分のホストを選択し、 ``Create Record Set`` をクリックします。

そして、以下のように Aレコードの設定を登録しました。（スクリーンショットの赤枠部分）

- ``Name``: 何も記入しない
- ``Type``: ``A - IPv4 address``
- ``Alias``: ``Yes``
- ``Alias Target``: CloudFront のホスト名。CloudFront の画面を参照し、 **CloudFront Distributions** の **Domain Name** の内容を記載します
- ``Routing Policy``: ``Simple`` (デフォルトのまま)
- ``Evaluate Target Health``: ``No`` (デフォルトのまま)

![Route53設定例.png](https://qiita-image-store.s3.amazonaws.com/0/280792/7d37ef93-f9c4-c4f1-7cf4-a87c876b3af7.png)


## 6. Certificate Manager (ACM) (必要に応じて実施)

独自ドメインとなるため、CloudFront の証明書のまま運用するとブラウザが文句を言いますので、SSL証明書を作成しました。

基本的に[こちら](https://dev.classmethod.jp/cloud/aws/acm-cloudfront-ssl/)を参考に作成してみました。が、ちょっと詰まったところだけメモしておきます

- **検証方法の選択** では、「**DNS の検証**」を選択します。「Eメール…」でも良いですが、届かない可能性があります。Route53 で作成している場合には **DNS の検証** を選択しておけば後は勝手に AWS 側でよしなにやってくれます。

作成後は、前述の CloudFront の Distribution の SSL 設定を変更しました。


## 7. テスト

ここまで長旅でした。お疲れ様です。最後に手元のクライアントからテストを行います。

### 7.1. 静的コンテンツの表示

ブラウザから ``https://{作成した独自ドメイン}/`` にアクセスしてアクセスできるか確認します。

### 7.2. API のテスト

以下のようなRESTリクエストを発行し、想定されたレスポンスが返却されるか確認します。
下記は curl サンプルです。

```sh:APIリクエストサンプル
$ curl -H "Content-Type: application/json" -H "x-api-key: {APIキー}" https://{作成した独自ドメイン}/api/master?mode=verbose
```

# ハマりポイント

- CloudFront 設定したのに有効にならない → 設定適用まで最大10分程度かかるので、気長に待つ。新規登録は時間かかるが、変更であれば即時適用される場合がある
- クエリパラメータが Lambda まで届かない → 問題箇所を切り分けてチェック。フィルタされている可能性があるのは、「CloudFront の API ビヘイビア設定」「API Gatewayのメソッドリクエスト」そして「API Gateway のマッピングテンプレート」いずれかの設定漏れ
- Certificate Manager からメールが来ない → 「DNSでの検証」に切り替える

# ランニングコスト

試験的な作成であり、自分以外だれもAPIをコールしていませんので

* Route53 でのドメイン代 (年額)
* Route53 HostedZone (x1) 代 ($0.5) 

が主ですね。実際にユーザ数が多いとどうなるかはわかりませんが、今の所はそんなに($10 を超えるような) 費用は発生しそうにはないです。

# まとめ（所感）

自前でフルスクラッチで構築するのに比べれば、手間は少なく&イニシャルコストが激安というのは魅力です。
が、それなりにAWSの勉強しておかないと右往左往しまくる羽目になるので、AWSの勉強大事だなと思いました（小学生並みの感想）。

設定が甘く、遊びの領域を出ない部分がありますが、「APIを作ってみたい！」な方の参考になれば幸いでございます。

# 参考文献

- [ゼロから作りながら覚えるAPI Gateway環境構築 (クラスメソッドさん)](https://dev.classmethod.jp/cloud/aws/getting-start-api-gateway/)
- [CloudFrontに複数オリジン（API GatewayオリジンとS3オリジン）の設定 (@horsewin さん)](https://qiita.com/horsewin/items/3209083d9fb3a8441895) 
- [Route 53 で独自ドメインをホストして、Amazon s3で静的ウェブサイトを公開 (@schabibi さん)](https://qiita.com/schabibi/items/bb2e6bd4e5a1f29eeb90)
- [[ACM] AWS Certificate Manager 無料のサーバ証明書でCloudFrontをHTTPS化してみた (クラスメソッドさん)](https://dev.classmethod.jp/cloud/aws/acm-cloudfront-ssl/)
- [ [AWS] Lambda + API Gatewayでサーバレスを始める (@naoki_koreeda さん)](https://qiita.com/naoki_koreeda/items/c2a32198c86e8d9a5bb6)
- AWS 各種サービスのマニュアル類