AmplifyのServerless containersを使って消しゴムマジックAPIをデプロイしてみた

この記事は[朝日新聞社 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/asahi)の第１日目の記事です。
これから毎日、朝日新聞社のエンジニアがバラエティに富んだテック記事をお届けします！

一部記事は[noteで運営中のテックブログ](https://note.com/asahi_ictrad)記事となります。
合わせてこちらもフォローいただけると幸いです。

https://note.com/asahi_ictrad

初日は[メディア研究開発センター](https://cl.asahi.com/)の倉井が、AWS/Amplifyを利用したサーバレスなコンテナAPIのデプロイに取り組んだ内容をお届けいたします！
***

### 超ざっくり作ったもの説明

AmplifyのServerless containersでデプロイしたAPIに
![スクリーンショット 2021-11-12 19.24.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277284/dfa53604-2922-5997-615f-870ef198ed0f.png)
![mask_image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277284/869aeeff-3e5e-56e2-5036-05f644c53149.png)
上の２つの画像を入力することで
![スクリーンショット 2021-11-12 19.22.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277284/33bf4237-b17a-dd68-ad01-c287b1bae4b2.png)
このように対象部分が消えた画像を返してくれるAPIをデプロイしました。
[消しゴムマジック](https://www.youtube.com/watch?v=PqTlzaIyWbs)という名称は、GoogleのPixel6の新機能名からお借りしました。

### 構成
アプリ全体はAWS Amplifyで構築しています。

Amplifyとはそもそも何なのかということはググっていただければと思いますが、簡単にいうと
「フロントエンドのウェブ/モバイル開発者が AWS でフルスタックアプリケーションをすばやく簡単に構築できるようにする専用のツールとサービスのセット」
です。

ウェブ/モバイルアプリケーションに対してDBや認証、APIの作成、CI/CD、MLバックエンドなどをある程度容易に準備するためのツールだと思っていただければOKです。
今回の趣旨ではないのでAmplifyそのものの説明はこの程度で。

今回のメイントピックであるServerless containersを実装することで出来上がるAPIのネットワーク構成図がこちらです。
![aha_network.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277284/8b2065aa-a9e3-c776-4aaf-5cfd3f48fca9.jpeg)
APIに関しても[Amplifyの機能](https://docs.amplify.aws/cli/usage/containers/)でデプロイしています。
データの流れは以下になります。

1. ユーザーがブラウザ上で①元画像と②消したい部分を塗りつぶしたマスク画像をapiに投げる
2. そのリクエストがログインユーザーからのものであることの認証を行ったのち、inpaint処理
3. 処理によって自動補完された画像のdataURIを返す

画像処理の部分に関しては、こちらのレポジトリを利用しています。

https://github.com/knazeri/edge-connect


上述のように①元の画像と②その画像の消したい部分のマスク画像を渡すことで、その部分を周辺のピクセルから修復（inpaint）してくれるものです。

### serverless containersデプロイの手順

今回はapiの作成のところから解説します。
基本的なAmplifyの初期設定方法については公式のチュートリアルをなぞっていけば問題ありません。

https://docs.amplify.aws/start/?sc_icampaign=start&sc_ichannel=docs-home/q/integration/react


またこの過程において、amplify configureを実行しますが、その中で

```bash
? Do you want to enable container-based deployments? Yes
```
としてコンテナベースのデプロイを有効にしておく必要があります。

もし設定できているか不安な場合は、

```bash
amplify configure project
```

を実行するとその中で上記の質問があるので、Yを入力すればOK。

次にcliからapiを追加します

```bash
amplify add api
? Please select from one of the below mentioned services: REST
? Which service would you like to use: API Gateway + AWS Fargate (Container-based)
? Provide a friendly name for your resource to be used as a label for this category in the project: hogehoge
? What image would you like to use: Custom (bring your own Dockerfile or docker-compose.yml)
? When do you want to build & deploy the Fargate task: On every "amplify push" (Fully managed container source)
? Do you want to access other resources in this project from your api?: No
? Do you want to restrict API access: Yes
```

ポイントをいくつか解説しておきます。

`? Which service would you like to use:
API Gateway + AWS Fargate (Container-based)`
を選択することでサーバレスなコンテナの構成を選択しています。

`? What image would you like to use:
 Custom (bring your own Dockerfile or docker-compose.yml)`
を選択することで、自分で０からコンテナの構成を設定することができます。

`? When do you want to build & deploy the Fargate task:
 On every "amplify push" (Fully managed container source)`
ここでOn every "amplify push"を今回は選択していますが、gitに変更がマージされた時にAPIを更新するようにCI&CDを整えることもできます。

`? Do you want to restrict API access:
 Yes`
ここでYesを選択することで、Amplifyのフロントアプリケーションで認証されたユーザーのみがこのAPIを発火することができるようになります。

上のように進めていくと下のような表示が出てきます

```bash
Successfully updated auth resource locally.
Successfully added resource hogehoge locally.

Next steps:
- Place your Dockerfile, docker-compose.yml and any related container source files in "amplify/backend/api/hogehoge/src"
- Amplify CLI infers many configuration settings from the "docker-compose.yaml" file. Learn more: docs.amplify.aws/cli/usage/containers
- Run "amplify push" to build and deploy your image
Successfully added resource hogehoge locally

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
```

ここまでくれば準備は完了です。

表示されているように`amplify/backend/api/hogehoge/src`が追加されていることが確認できると思います。
あとは表示されているようにdocker関係のファイルを用意して、処理部分のコンテナを整備してあげればOKです。

### コンテナの作成

今回は以下のようなディレクトリ構成で処理部分のコンテナを用意しました。

```bash
|--docker-compose.yml
|--python
|  |--Dockerfile
|  |--requirements.txt
|  |--src
|  |  |--api.py
|  |  |--main.py
|  |  |--（上のgithubレポジトリ由来のディレクトリ複数）
```

docker-compose.ymlは以下のようにしました。

```yaml
version: "3.8"
services:
  python:
    build:
      context: ./python
      dockerfile: Dockerfile
    networks:
      - public
      - private
    restart: always
    tty: true
    ports:
      - 8000:8000
networks:
  public:
  private:
```

requirement.txtは以下の通りです。

```yaml
numpy ~= 1.20.1
scipy ~= 1.1.0
future ~= 0.16.0
matplotlib ~= 2.2.2
pillow >= 6.2.0
opencv-python ~= 3.4.0
scikit-image ~= 0.14.0
torch ~= 1.0.0
torchvision ~= 0.2.1
uvicorn ~= 0.11.8
fastapi ~= 0.61.1
pydantic == 1.7.3
gcc7
pyaml
loguru
```
今回はuvicornでサーバーを立て、その上にfastapiでAPIを準備しています。
その他にも画像処理に必要なモジュールを用意します。

最後にDockerfileは以下のようにしました。

```yaml
FROM public.ecr.aws/ubuntu/ubuntu:20.04

ENV DEBIAN_FRONTEND=noninteractive

# apt-get
RUN apt-get update \
    && apt-get install -y wget build-essential gcc zlib1g-dev \
    && apt-get install -y libssl-dev libncurses5-dev libsqlite3-dev libreadline-dev libtk8.6 libgdm-dev libdb4o-cil-dev libpcap-dev \
    && apt-get install -y curl wget make xz-utils file sudo unzip libssl-dev libffi-dev imagemagick\ 
    && apt-get install -y python3 python3-pip python3-setuptools liblzma-dev libgl1-mesa-glx libx11-dev tk-dev python3-tk

RUN apt-get update \
    && apt-get install -y vim git \
    && apt-get install -y language-pack-ja-base language-pack-ja 

# language = ja
ENV LANGUAGE ja_JP.UTF-8
ENV LC_ALL ja_JP.UTF-8
ENV LANG=ja_JP.UTF8

# python setup
WORKDIR /root/
RUN wget https://www.python.org/ftp/python/3.7.7/Python-3.7.7.tgz \
        && tar zxf Python-3.7.7.tgz \
        && cd Python-3.7.7 \
        && ./configure \
        && make altinstall
ENV PYTHONIOENCODING "utf-8"

WORKDIR /usr/local/bin/
RUN ln -s python3.7 python
RUN ln -s pip3.7 pip

# package install
WORKDIR /root/
# copy the dependencies file to the working directory
COPY requirements.txt .
# install dependencies
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
COPY src/ .

# FastAPIを8000ポートで待機
CMD ["uvicorn", "api:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]
```

今回はpythonをwgetでインストールしていますが、時間がかかるのでpython-busterなどを利用してもいいかもしれません。
ただしその場合Debian系統となるので、単純な置き換えでは動かない点に注意です。

### API部分の実装

上述のDockerfileにおいて`COPY src/ .`でsrc以下のファイルをrootにコピーしています。
そして最後の行でuvicornを実行し、`api:app`でapi.pyのappを叩くことを指定しています。

肝心のapi.pyですが、以下のように実装しました。

```python
from fastapi import FastAPI
from pydantic import BaseModel, Field
from starlette.middleware.cors import CORSMiddleware
import os
import base64
from urllib import request
from main import main
from loguru import logger

app = FastAPI()

origins = [
    'http://localhost:3000', # 開発時のAmplifyフロント
    'https://hogehoge', # デプロイした時のドメイン
]

# CORS対応
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class InputData(BaseModel):
    ori_image_data_uri: str = Field(..., description="元画像dataURI")
    mask_image_data_uri: str = Field(..., description="画像dataURI")

class Document(BaseModel):
    status_code: int = Field(..., description="ステータスコード", example = "200")
    output: str = Field(..., description="出力、成功時にはdataURI、エラー時にはエラーメッセージ", example = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...")

def rgba_to_rgb(imgfile:str):
    os.system(f"convert {imgfile} png24:{imgfile}")

def rgba_to_ga(imgfile:str):
    os.system(f"convert {imgfile} -type GrayscaleMatte -depth 1 {imgfile}")

@app.get('/healthcheck')
def healthcheck():
    return 'OK'

@app.post("/edge-connect", response_model=Document)
def inpaint(data: InputData):
    """[summary]

    Args:
        ori_image_data_uri (str): 元画像dataURI
        mask_image_data_uri (str): マスク画像dataURI

    Returns:
        inpainted_img (str): inpaintされた画像のdataURI
    """

    with request.urlopen(data.ori_image_data_uri) as response:
        ori_image_data = response.read()
    with open("/tmp/ori_image.png", "wb") as f:
        f.write(ori_image_data)

    with request.urlopen(data.mask_image_data_uri) as response:
        mask_image_data = response.read()
    with open("/tmp/mask_image.png", "wb") as f:
        f.write(mask_image_data)
    
    rgba_to_rgb('/tmp/ori_image.png')
    rgba_to_ga('/tmp/mask_image.png')

    
    try:
        main(mode=2)
        with open('/tmp/output_image.png', 'br') as f:
            b64_img = base64.b64encode(f.read())
            return_dataURI = "data:image/png;base64,{}".format(b64_img.decode('utf-8'))
        status_code = 200
        output = return_dataURI
        logger.info("statuscode:200 で終了")
    except:
        status_code = 500
        output = 'Internal sever error'
        logger.info("statuscode:500 で終了")

    document = Document(
        status_code = status_code,
        output = output
    )

    return document
```

postでデータを受け取って、main.pyのmain関数を実行するような形式にしました。

受け取るデータと返すデータの型や説明をpydanticのBaseModelを用いて定義してあげることで、

- 実行時の型情報の提供
- 不正なデータにはユーザーフレンドリーなエラーを返す

といったことが実現できます。（下記から引用しています）

https://qiita.com/koralle/items/93b094ddb6d3af917702

main.pyやその他のファイルに関しては[参考にしたレポジトリ](https://github.com/knazeri/edge-connect)のものをそのまま利用しています。

### ローカルでの実行

ローカルで動かしたい場合は

```bash
docker-compose up -d
```

を実行することでコンテナが立ち上がります。（Docker環境が構築されている必要があります）

起動後に
http://localhost:8000/docs#
にアクセスすれば、Swagger UIによるブラウザ上でのAPIのテストが可能です
![スクリーンショット 2021-11-12 19.34.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277284/ed23faf4-2117-dd76-f3be-9ff57e29764e.png)
### デプロイ

ローカルでの処理成功が確認できたら、amplify pushを実行します。
すると長いデプロイ作業が走り、最終的にdeployが成功したというような旨の表示がされ、今回デプロイしたAPIのエンドポイントが表示されます。

あとはアプリケーション内でこのエンドポイントにアクセスするように設定すれば、消しゴムマジックAPIの完成です。

### できたものを眺める

改めて、出来上がったAPIで処理された画像と元の画像を並べてみました。
画像処理の部分は今回の趣旨ではありませんが、ある程度綺麗に消えているのが確認できます。
![スクリーンショット 2021-11-12 19.20.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277284/0cfff624-ae63-5a15-c0be-66dff54ac971.png)
ちなみにフロントでは次のような手順を経て元画像とマスク画像を用意し、APIを叩いています。
![画面収録 2021-11-15 13.53.18.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277284/7667766d-cb99-2d88-7171-0209485092f3.gif)
元画像と処理後の画像の透過度を徐々に入れ替えてあげることで、こんなアハ動画の作成もできます。
![画面収録 2021-11-12 19.21.01 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277284/d69b5dae-b504-b53f-d651-46d12d0af54a.gif)
実は、このAPIで実現したかったことは「簡単にアハ動画を作成できる機能をアプリケーションに実装すること」で、上のような徐々に物体が消えていく動画や、逆再生による徐々に物体が出現する動画が作成できるようになりました。
### こちらで使われていきます

まだ本実装には至っていませんが、このアハ動画作成機能はQukkerというクイズCMSに搭載されていく予定です。

Qukkerはユーザーが簡単にクイズ（４択クイズやアハ動画、クロスワードなど）を作って共有できるサービスです。

現時点では、４択クイズだけにはなりますが、こちらで体験いただけます。

https://qukker.ictrad.io/quizplaylist/e6995f7a-7583-4641-85ba-8680a275338a/play

今のところ社内限定サービスとなっていますが、ゆくゆくは社外への展開を検討しています。

アハ動画を見かけたら、こんなサービスを朝日新聞社が作っていたっけと思い出していただけると嬉しいです！!

***

朝日新聞社では、技術職の中途採用を強化しています。
ご興味のある方は下記リンクから希望職種の募集ページに進んでください。
皆様からのご応募、お待ちしております！

https://www.asahishimbun-saiyou.com/information/career