AWSでゴミ分別APIを作った




# はじめに

AWS環境にゴミ分別をしてくれるAPIを作ります。

Cloud9を使用します。試してみた記事は以下です。

[Cloud9でPythonを試してみる - Qiita](https://qiita.com/estaro/items/53681eb8e9e4f240f10f)


# 設計

分別したいゴミをパラメータで渡すと結果を返すAPIを作ります。

ごみの分別ですが、川崎市のゴミ分別のデータセットがCCライセンスで公開されているのでそれを使用します。

[川崎市：オープンデータ化に向けた取組](http://www.city.kawasaki.jp/shisei/category/51-7-4-0-0-0-0-0-0-0.html)

データセットは更新されることがありそうです。

このことから、APIは以下のようにバージョンも指定できるようにします。

```
GET /garbage?word=皿&v=300110
```

# 開発

## lambda関数を作成する

cloud9からAPI Gatewayと連携したLambdaを作成します。

[AWS Resources]からLambdaの追加アイコンを選択。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/8f4514af-1a10-a1b7-310d-98b889580575.png)

名前は適当。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/2e09f4b7-74ff-69ca-e49e-d6549cbc173d.png)

テンプレートはAPI Gatewayを使っているものを選んでみました。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/0c36812d-634f-a06e-ff33-43c18dd1281f.png)

エンドポイントのパスも適当に。セキュリティは後々考えます。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/a08edf32-a991-f1be-f4cc-cfbf00930065.png)

開発なのでメモリは最低。Roleは自動生成します。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/0433b1d0-3530-989b-8638-5636127cddcf.png)

Finish!

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/944c9031-18a2-2f53-271e-2cd6bf44fd17.png)

できました。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/3e08c281-d5f5-7b8f-29da-c9febb1992c4.png)


## Lambda関数を実装する

テンプレートとして利用したものはDynamoDBを使用していますが、ここではそこまでする必要はないので、インスタンス単位でCSVファイルを読み込むこととします。

あとは読み込んだファイルの内容からマッピングを返すだけなので、説明することはありません。

注意点はコピーしてきた公開されているCSVファイルがShift-JISである点くらいです。


```py:lambda_function.py
# -*- coding: utf-8 -*-
import csv
import json
import os
import logging

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

_cache = None


class AppError(Exception):
    """
    汎用エラークラス
    """
    def __init__(self, message):
        self.message = message


def lambda_handler(event, _):
    """
    lambda

    :param event:
    :param _: context
    :return:
    """
    print("Received event: " + str(event))

    version = event.get('queryStringParameters', {}).get('v', None)
    word = event.get('queryStringParameters', {}).get('word', None)

    # 入力チェック
    if not version or not word:
        return respond(None, err=AppError('invalid parameter'))

    # データベースの取得
    if not _update_cache(version):
        return respond(None, err=AppError('Unsupported version "{}"'.format(version)))

    result = _cache.get(version, {}).get(word, '分類できませんでした')
    return respond({
        'version': version,
        'classification': result,
    })


def _update_cache(version):
    """
    データベースを更新する

    :param version:
    :return:
    """
    global _cache

    if not _cache:
        _cache = {}

    if version not in _cache:
        _cache[version] = {}
        filename = './' + version + '.csv'
        if os.path.exists(filename):
            with open(filename, 'r', encoding='shift_jis') as f:
                reader = csv.reader(f)
                next(reader)
                # 0 ,1     ,2     ,3   ,4     ,5             ,6             ,7               ,8
                # ID,頭文字,品目名,読み,類似語,出し方（一覧）,出し方（詳細）,出し方のポイント,URL1
                for row in reader:
                    out = row[5]
                    if row[6].strip():
                        out += 'または' + row[6]
                    _cache[version][row[2]] = out
                    _cache[version][row[3]] = out
                    if row[4].strip():
                        synonyms = row[4].split(' ')
                        for synonym in synonyms:
                            _cache[version][synonym] = out
                return True
        else:
            logger.warning('file not exist.')
            return False
    else:
        # すでに取得済み
        return True


def respond(res, err=None):
    """
    API Response

    :param err:
    :param res:
    :return:
    """
    return {
        'statusCode': '400' if err else '200',
        'body': err.message if err else json.dumps(res, ensure_ascii=False, indent=2),
        'headers': {
            'Content-Type': 'application/json',
        },
    }
```



## サーバーレスの設定

デプロイ時の設定はtemplate.yamlで行います。テンプレートで作成済みです。
ここではMemorySizeを小さく、受け取るMethodをGETのみにする、といった変更を行いました。

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: An AWS Serverless Specification template describing your function.
Resources:
  garbage:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: garbage/lambda_function.lambda_handler
      Runtime: python3.6
      Description: ''
      MemorySize: 128
      Timeout: 15
      CodeUri: .debug/
      Events:
        LambdaMicroservice:
          Type: Api
          Properties:
            Path: /garbage
            Method: GET
  garbagePermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName:
        'Fn::GetAtt':
          - garbage
          - Arn
      Principal: apigateway.amazonaws.com
      SourceArn:
        'Fn::Sub': 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:*/*/*/*'
```


## デプロイ

IDEのLambdaメニューからデプロイします。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/2a0daa3b-d730-e5c5-115b-60ea9e59ad2f.png)

## 確認

AWSのAPI GatewayからURLを確認して実行してみます。

![image](https://qiita-image-store.s3.amazonaws.com/0/197945/9720d9bd-05de-08ee-d63f-de477d92e779.png)

```
https://****************.us-west-2.amazonaws.com/Stage/garbage?word=パソコンケース&v=300110
```

ブラウザなどから取得したURLを入力して以下のようなレスポンスが返ってくればOKです。

```json
{
  "version": "300110",
  "classification": "普通ごみまたは粗大ごみ"
}
```


# おわりに

このAPIを使用した企みは別の記事で行います。

# 参考

[AWS Cloud9 Integrated Development Environment (IDE) で AWS Lambda 関数を操作する - AWS Cloud9](https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/lambda-functions.html#lambda-functions-prepare)

[Python3 で CSV の読み書きをする方法 – Shift_JIS と UTF-8 対応サンプルコード付 | Crane & to.](https://www.craneto.co.jp/archives/1309/)

[19.2. json — JSON エンコーダおよびデコーダ — Python 3.6.5 ドキュメント](https://docs.python.jp/3/library/json.html)