FastAPIをサーバレス環境にCI/CDする(Google編)

# はじめに

こちらでは、Python で FastAPIを使用したREST APIサーバをたてて、サーバレス環境にCI/CDする手順の確認を目指しています。手始めに、Google Cloud Ploatform で行いました。pipenv, docker, gcloud の復習をしながら、メモを書いておきます。

元ネタはこちらですが、自分はあまりconsole を使わずに作業しました。

https://towardsdatascience.com/deploy-a-dockerized-fastapi-app-to-google-cloud-platform-24f72266c7ef


# 内容

## Deployするものの用意

### FastAPIのプログラム

まずはdeploy する FastAPIのプログラムを用意します。

```Python:main.py
from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def hello():
    return {"message" : "Hello,World"}
```

これはfastapi, uvicorn をpip install してローカルに動作させることができます。が、、、

### Python 環境はPipenv

ここでは Pipenv で用意した仮想環境で動作させ、それをそのままDocker image にします。Pipenvは普段使っていないので間違っているかもしれませんが、自分の理解では、pipenv install でpip install と同様にしてインストールすると、それが、Pipenv.lockというファイルに残され、Docker image 作成時などに同じ環境を再現するときに使えるもののようです。

```shell
$ pipenv --python 3.9
Creating a virtualenv for this project…
Using /usr/bin/python3.9 (3.9.5) to create virtualenv…
⠋created virtual environment CPython3.9.5.final.0-64 in 1339ms
  creator CPython3Posix(dest=/home/username/.local/share/virtualenvs/2022-05-05_fastapi_gcp-RDCyasYb, clear=False, global=False)
  seeder FromAppData(download=False, pip=latest, setuptools=latest, wheel=latest, pkg_resources=latest, via=copy, app_data_dir=/home/username/.local/share/virtualenv/seed-app-data/v1.0.1.debian.1)
  activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator

Virtualenv location: /home/username/.local/share/virtualenvs/2022-05-05_fastapi_gcp-RDCyasYb
Creating a Pipfile for this project…
```

どうやら ~/.local/share/virtualenv 以下になにか作られているっぽいです。ローカルには Pipfile が作られています。
必要なものをインストールします。

```terminal
$ pipenv shell
$ pipenv install fastapi gunicorn uvicorn
```

Pipfile の ```[packages]```が更新されますが、同時に Pipfile.lock というファイルも生成されます。
普段は、``` uvicorn main:app --port 8080 --reload``` のように実行しますが、ここでは gunicorn をインストールしました。

```shell
$ which gunicorn
/home/username/.local/share/virtualenvs/2022-05-05_fastapi_gcp-RDCyasYb/bin/gunicorn
```

確かに仮想環境にインストールされています。

実行してみます。

```shell
$ python -m gunicorn --workers 1 --worker-class uvicorn.workers.UvicornWorker main:app
2022-05-05 19:14:40 +0900] [130973] [INFO] Starting gunicorn 20.1.0
[2022-05-05 19:14:40 +0900] [130973] [INFO] Listening at: http://127.0.0.1:8000 (130973)
[2022-05-05 19:14:40 +0900] [130973] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2022-05-05 19:14:40 +0900] [130974] [INFO] Booting worker with pid: 130974
[2022-05-05 19:14:40 +0900] [130974] [INFO] Started server process [130974]
[2022-05-05 19:14:40 +0900] [130974] [INFO] Waiting for application startup.
```

ブラウザで ```locaohost:8000/docs``` を開き、APIのテストができることを確認します。


### Docker image の作成

Dockerfileを用意してimage を作成します。引数でポート番号を渡すことにします。

```Dockerfile
FROM python:3.9-slim
ENV APP_HOME /app
ARG PORT=80
ENV PORT=${PORT}

WORKDIR $APP_HOME
COPY . ./
COPY main.py /app/main.py
RUN pip install pipenv
RUN pipenv install --deploy --system
CMD exec gunicorn --bind :${PORT} --workers 1 --worker-class uvicorn.workers.UvicornWorker  --threads 8 main:app
```

まずビルドします。

```shell
$ docker build -t testapi:test --no-cache=True --build-arg PORT=8000 .
$ docker images
REPOSITORY                          TAG              IMAGE ID       CREATED          SIZE
testapi                             test             fcb704e14223   11 seconds ago   240MB
python                              3.9-slim         8c7051081f58   2 weeks ago      125MB
```

実行します。

```
$ docker run --rm -p 8001:8000 --name testapi1 -d testapi:test
$ docker logs testapi1 -f
[2022-05-05 10:45:45 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2022-05-05 10:45:45 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2022-05-05 10:45:45 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2022-05-05 10:45:45 +0000] [7] [INFO] Booting worker with pid: 7
[2022-05-05 10:45:45 +0000] [7] [INFO] Started server process [7]
[2022-05-05 10:45:45 +0000] [7] [INFO] Waiting for application startup.
[2022-05-05 10:45:45 +0000] [7] [INFO] Application startup complete.
```

これで```localhost:8001``` で表示されます。

### Docker-compose

docker-compose に整理しておきます。実はGoogle Cloudではあまり使われないようなのですが、一応。
version3.9 を使うには cocker-compose の v1.29.2が必要なので、インストールした。

```
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod o+x /usr/local/bin/docker-compose 
```

ポート番号をDockerfile でPORTという変数にbuild-arg で渡すために、docker-compose build するときに環境変数にします。そのために、variables.env に書きます。屈折している気もするが動いたので。

```variables.env
PORT=8000
```

というファイルを用意しておき、YAMLファイルは以下で動いた。

```YAML:docker-compose.yaml
version: "3.9"

services:
  apisvr:
    container_name: testapi
    env_file: ./variables.env
    build:
      context: .
      dockerfile: ./Dockerfile
      args:
        - PORT=${PORT}
    ports:
      - "8001:8000"
    restart: always
    tty: true
```

これでいつものパターンで動かします。

```
$ docker-compose build
$ docker-compose up -d
$ docker-compose logs apisvr -f
```

同じようにlocalhost:8001 で動きます。
ローカルでの準備おしまい。


## GCPの Cloud Run に deploy する(CloudBuild使用)

### 用意

project とか確認しておく。とりあえずユーザアカウントの認証と、プロジェクトを指定する。

```
$ gcloud config configurations list
```

認証情報についてはこちら￥が詳しい。勉強になった。アカウントの切り替えについても書いてある。情報は~/.config/gcloud/credentials.db にあり、```sqlite3 credentials.db```でローカルに保持している情報も確認できるとは知らなかった。

https://christina04.hatenablog.com/entry/gcp-auth

```
$ gcloud auth login --no-launch-browser
```

で得たURLをブラウザに貼り付け、正しいgoogle account でログインする。

```
$ gcloud config get-value project
```

で正しいプロジェクトになっているかを見る。そうでなければ、

```
$ gcloud projects list
$ gcloud config set project PROJECT_ID
```

PROJECT_IDはGCPのProject IDが入ります。今回プロジェクトを新規作成したのだが、project name ではなく、project ID でないと set project できなかった。もともとそうなのか、良くわからない。


### Registry のPush する

```
$ gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/YOUR_IMAGE:YOUR_TAG
```

実行すると、最後にSOURCE としてstorage に収められているtgzファイル、IMAGE 名が表示されます。

```
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ID                                    CREATE_TIME                DURATION  SOURCE                                                                                        IMAGES                             STATUS
6235a5cf-d83b-4bc1-8e99-c41eaadba769  2022-05-05T13:02:42+00:00  45S       gs://PROJECT_ID_cloudbuild/source/1651755760.558377-3e5f641a15ed4e828b89ba567de3e229.tgz  gcr.io/PROJECT_ID-349211/YOUR_IMAGE:TAG  SUCCESS

```

これはどうやらローカルのリポジトリにはなく、docker images してもリストには表示されません。あと、```--build-arg```で渡していた値を渡せていないくて、実は少し嫌です。以下には、cloudbuild.yaml を書くように言っていますが、それはまたあとで行います。

https://til.simonwillison.net/cloudrun/using-build-args-with-cloud-run

とりあえず、docker コマンドでローカルでbuild してからpush することにしました。

```
$ docker build -t gcr.io/YOUR_PROJECT_ID/YOUR_IMAGE:TAG --no-cache=True --build-arg PORT=8000 .
```

ローカルにimage が作られますが、このとき、プロジェクト名が違っていてもそのまま作られていまいます。（経験^^;)
できたら、registry に push します。

```
docker push gcr.io/PROJCT_ID/MY_IMAGE:MY_TAG
```

最初、ローカルでdocker build して tag に gcr.io/... をつけて、registry に push しようとしましたが、できませんでした。これは以下でできるようになりました。

```
$ gcloud auth configure-docker
Adding credentials for all GCR repositories.
WARNING: A long list of credential helpers may cause delays running 'docker build'. We recommend passing the registry name to configure only the registry you are using.
After update, the following will be written to your Docker config file located at
[/home/USERNAME/.docker/config.json]:
 {
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud"
  }
}

Do you want to continue (Y/n)?
```

https://cloud.google.com/container-registry/docs/advanced-authentication#gcloud-helper

以下でdeploy します。

```
$ gcloud run deploy --image gcr.io/PROJECT/IMAGE:TAG --platform managed
```

サービス名とregion を聞かれるので、適当に入れておきます。

```
Allow unauthenticated invocations to [apisvr] (y/N)?  y
```
と聞かれて、めんどうくさいので y としました。
Service URL が表示されるので、開くと確かに同じものが表示されました。docs とするとAPIテストもできます。

実は、deploy するとき、ポート番号をしていないのですが動いています。なぜなのか、よくわかりません。


### CI/CD する

これは、cloudbuild.yaml にbuild/deploy の手順を書いて、 GCPのCloudBuild というサービスでせってしたtrigger で動かす、というものです。cloudbuild.yaml は ```docker builds submit --config cloudbuild.yaml ```とローカルで実行することもできます。


```YAML:cloudbuild.yaml
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
  dir: work_dir
  args: ['build', '-t', 'gcr.io/${PROJECT_ID}/apisvr:0.0.0', '.']
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
  args: ['push', 'gcr.io/${PROJECT_ID}/apisvr:0.0.0']
# Deploy container image to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
  entrypoint: gcloud
  args:
  - 'run'
  - 'deploy'
  - 'apisvr'
  - '--image'
  - 'gcr.io/$PROJECT_ID/apisvr:0.0.0'
  - '--region'
  - 'asia-northeast1'
images:
- gcr.io/${PROJECT_ID}/apisvr:0.0.0
```

dirはファイルがあるディレクトリのパスです。ここではgit repository でroot直下にある work_dir というフォルダにDockerfile やmain.py らがあることを想定しています。これを

```
$ gcloud builds submit --config=cloudbuild.yaml
```

すると、最初、push するときに

> Step #2: ERROR: (gcloud.run.deploy) PERMISSION_DENIED: Permission 'run.services.get' denied on resource 'namespaces/PROJCT_ID/services/IMAGE_NAME' (or resource may not exist).

と言われましたが、これはCloudBuild に必要な権限を付与していないためです。
console.cloud.google.com の CloudBulid のページで、CloudRun に権限を与えればOKです。ここにしっかりと書いてありました。

https://cloud.google.com/build/docs/deploying-builds/deploy-cloud-run?hl=ja#cloud-run_2


いざ、CloudBuild のページでtrigger を設定します。Github repository への接続を行い、cloudbuild を指定します。

https://blog.truaro.dev/continuous-deployment-with-cloud-build/

# まとめ

とりあえず動きました。FastAPIを実践で使ったことがないのですが、これで何とか使えそうな気がします。

次は、

- 同じものをAWS Lambda で動かす
- CloudRun で認証をつける

ができたらいいかな。
(2022/05/05)