Route53, GCE, React, FastAPIを用いて簡単なWebアプリを作製する 【前編】

# なんの記事？
* バックエンドを今までメインで触っていたのですが、実際にブラウザ上で動くWebアプリも作製したいと最近考えるようになりました。
* 実際にGCPのサービスを用いて、バックエンドにFastAPIを利用している簡単なサービスを勉強がてら作ってみました。
* どのサービスも（特にRoute53やGAEは）初めて利用するので、もし間違いなどあれば、指摘いただけると幸いです。

# 今回作るもの
以下のように、arXivから論文を取得し、タグ付けしたものを一覧表示するWebアプリケーションを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/82d8f2fa-3604-de1b-6d86-9bbcd4d70f3d.png)

# 手順
以下の手順で今回は作製しました。

* Route53を用いたDNS解決、アドレス取得
* バックエンド実装
* フロント実装
* 結合
* デプロイ

## Route53によるDNS解決
早速進めていきたいと思います。まずは、Route53で先にドメインを購入・取得し、開発のモチベーションを上げましょう。

https://console.aws.amazon.com/route53/v2/home#GetStarted

からRoute53のページに行き、ドメインを登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/717594f8-ef54-bc39-3ab1-d7aabc83f026.png)

安いドメイン候補を今回は利用しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/85b91fca-9b46-67aa-79dd-82ec10bea450.png)

支払情報を登録した後、注文を完了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/36427c36-b390-4763-13de-f5f2fc55d932.png)

ドメイン登録が進行中の間に、他の作業を進めましょう。

## バックエンド実装
今回はarXivのAPIを参照して得られた結果にタグ付をし、フロントへと渡す処理をバックエンドで書きたいと思います。

FastAPIを経由せずとも、フロントから直接叩く方法でも良かったのですが、今回はバックエンド＋フロントエンドを分離する構成を学びたいというモチベーションもあり、この構成にします。リポジトリも今回は独立にします。

ではバックエンドを作成していきます。

### Dockerfile作成
https://fastapi.tiangolo.com/ja/deployment/docker/

上記の公式リンクを参考にDockerfileを作成していきます。


```Dockerfile:Dockerfile
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7

RUN apt-get -y update && apt install -y build-essential

ARG HOME='/root'
ARG project_dir=/projects/
RUN apt-get install -y git
RUN pip install --upgrade pip && pip install autopep8
ADD requirements.txt .
RUN pip install -r requirements.txt
COPY . $project_dir
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "trace"]

```

立ち上げ時のapp.pyも作成します。

```python3:app.py
import uvicorn
from typing import Optional
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
    return {"item_id": item_id, "q": q}


if __name__ == '__main__':
    uvicorn.run("app:app", host='0.0.0.0', port=8000,
                log_level="trace", debug=True)

```

ビルドしてコンテナを起動します。

```
$ docker build -t arxiv_checker:latest .
$ docker run -d --rm --name arxiv_checker_debug -it -v ${PWD}:/projects -p 8000:8000 arxiv_checker
$ docker ps -a
>> ff005de9d501   arxiv_checker      "uvicorn app:app --r…"   28 seconds ago   Up 27 seconds              80/tcp                                                 arxiv_checker_debug

```

ここまでで起動が確認できました。
実際に　`localhost:8000` において

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/60e007d9-0e7e-c993-7351-decb377b0d9d.png)

が確認できました。また、デバッグを行いやすくするため、`uvicorn`起動時に`--reload`オプションを指定して自動起動を行わせ、かつ `docker run` 時にホストの開発コード部分のボリュームを `${PWD}:/projects` でマウントしています。

### arXivAPIを用いた論文取得
バックエンド側で今回この処理を書いていきます。
構成は今回、以下のようにすることで、バックエンドプロジェクトとAPIとを分離しました。
このように行うことで、バックエンドのロジックが複雑・多大となったとしても、切り分けが容易だと考えました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/51246b65-bcd6-2f7c-b774-1869ff6277e2.png)

では `backend` 下にロジックを書いていきます。が、今回はデプロイやフロントとの結合が学習のメインです。
ですので、バックエンドについては割愛します。書いたものを以下に用意しました。

https://github.com/izuna385/arxiv-checker-backend

バックエンド側では主に

* arXivAPIから、自然言語処理の関連論文の取得
* アブストを用いたタスク抽出
* アブストからの固有表現抽出

をspaCyを用いて行っています。


```python:extractor.py
from typing import List
import spacy
from backend.config import NLP_TASKS

class Extractor:
    def __init__(self, spacy_model: str = 'en_core_web_sm'):
        self.nlp = spacy.load(spacy_model)

    def extract_ne(self, sentence: str) -> List[str]:
        doc = self.nlp(sentence)
        return list(set([X.text for X in doc.ents if X.label_ not in ["CARDINAL", "PERCENT"]]))

    def extract_task(self, sentence: str) -> List[str]:
        return list(set([task for task in NLP_TASKS if task in sentence]))


```

### pytestを用いた、動く仕様書の作成
今回はpytestを使い、TDDをバックエンドでお試しで行いました。テストコードがそのまま仕様書となるように、`test_{テストしたい機能や仕様}`となるコードを作成しました。

```python:test_extractor.ypy
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest
from backend.extractor import Extractor
import pdb

class TestExtractor:
    @pytest.fixture
    def extractor_class(self):
        extractor_class = Extractor()
        return extractor_class

    @pytest.fixture
    def sentence_example(self):
        sentence_example = 'We introduce a new language representation model called BERT, which stands ' \
                        'for Bidirectional Encoder Representations from Transformers. Unlike recent language ' \
                        'representation models, BERT is designed to pre-train deep bidirectional representations ' \
                        'from unlabeled text by jointly conditioning on both left and right context in all ' \
                        'layers. ' \
                        'As a result, the pre-trained BERT model can be fine-tuned with just one additional ' \
                        'output ' \
                        'layer to create state-of-the-art models for a wide range of tasks, such as question ' \
                        'answering and language inference, without substantial task-specific architecture ' \
                        'modifications. BERT is conceptually simple and empirically powerful. It obtains new ' \
                        'state-of-the-art results on eleven natural language processing tasks, including pushing ' \
                        'the GLUE score to 80.5% (7.7% point absolute improvement), MultiNLI accuracy to 86.7% ' \
                        '(4.6% absolute improvement), SQuAD v1.1 question answering Test F1 to 93.2 (1.5 ' \
                        'point absolute improvement) and SQuAD v2.0 Test F1 to 83.1 ' \
                        '(5.1 point absolute improvement).'

        return sentence_example

    class TestExtractNe:
        def test_nlp要素を含む複文からエンティティのリストを返却する(self, extractor_class, sentence_example):
            assert 'ELECTRA' in extractor_class.extract_ne(sentence_example)

    class TestExtractTasks:
        def test_nlpタスクを含む複文からnlpタスクのリストを返却する(self, extractor_class, sentence_example):
            assert 'question answering' in extractor_class.extract_task(sentence_example)

```

このように書くメリットとしては、

* `test_check_ne_extract_from_one_sentence` など、テストのメソッド名について悩む可能性が低くなる
* テストコードがそのまま仕様書となる。
* テストを走らせた時、以下のように、どの機能が現在仕様を満たしていないかがひと目で分かる。

などがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/8fe45885-6e7f-3b9b-6697-2cf7191127c0.png)

### API実装
バックエンドを作成したので、それらのクラスを組み合わせて、APIを実装します。

今回は、1より大きい整数をクエリパラメータとして受け取り、その分だけ関連する自然言語処理の論文を返却するようなAPIを作成します。
ただ返却するだけでは、arXivAPIと内容が重複するので

* 固有表現抽出を用いた、論文へのタグ付け
* 論文が扱うタスク抽出

を行った後、それらのデータをjsonに付与し、返却したいと思います。

```python:app/main.py
import uvicorn
from fastapi import FastAPI
from backend.arxiv_api import ArxivApiClass
from backend.extractor import Extractor

app = FastAPI()
arxiv_api = ArxivApiClass()
extractor = Extractor()

@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/api/papers/")
def read_nlp_papers(num: int = 10):
    papers = arxiv_api.call_nlp_papers(max_results=num)
    papers = [arxiv_api.extract_title_and_summary_and_comment_from_paper(paper) for paper in papers]

    for paper in papers:
        paper.update({
            "task": extractor.extract_task(paper["summary"]),
            "ne": extractor.extract_ne(paper["summary"])
        })

    return {"papers": papers}


if __name__ == '__main__':
    uvicorn.run("app.main:app", host='0.0.0.0', port=8000,
                log_level="trace", debug=True)

```

`curl localhost:8000/api/papers/?num=3` などでクエリパラメータを渡し、jsonの返却を確認しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357766/f1b1f6b5-1d0e-ba3d-e282-1195f0464e2b.png)

ここまででバックエンドの実装を完了しました。
次回は、フロントエンドの実装とデプロイになります。

https://qiita.com/izuna385/items/64812520a1f2f79d2154

## リポジトリ

https://github.com/izuna385/arxiv-checker-backend

## 参考文献

https://qiita.com/ichiroex/items/ee78d742170cf4f5dec4