はじめに
こちらでは、Python で FastAPIを使用したREST APIサーバをたてて、サーバレス環境にCI/CDする手順の確認を目指しています。手始めに、Google Cloud Ploatform で行いました。pipenv, docker, gcloud の復習をしながら、メモを書いておきます。
元ネタはこちらですが、自分はあまりconsole を使わずに作業しました。
内容
Deployするものの用意
FastAPIのプログラム
まずはdeploy する FastAPIのプログラムを用意します。
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 作成時などに同じ環境を再現するときに使えるもののようです。
$ 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 が作られています。
必要なものをインストールします。
$ pipenv shell
$ pipenv install fastapi gunicorn uvicorn
Pipfile の [packages]
が更新されますが、同時に Pipfile.lock というファイルも生成されます。
普段は、 uvicorn main:app --port 8080 --reload
のように実行しますが、ここでは gunicorn をインストールしました。
$ which gunicorn
/home/username/.local/share/virtualenvs/2022-05-05_fastapi_gcp-RDCyasYb/bin/gunicorn
確かに仮想環境にインストールされています。
実行してみます。
$ 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 を作成します。引数でポート番号を渡すことにします。
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
まずビルドします。
$ 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 に書きます。屈折している気もするが動いたので。
PORT=8000
というファイルを用意しておき、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
でローカルに保持している情報も確認できるとは知らなかった。
$ 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 を書くように言っていますが、それはまたあとで行います。
とりあえず、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)?
以下で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
とローカルで実行することもできます。
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です。ここにしっかりと書いてありました。
いざ、CloudBuild のページでtrigger を設定します。Github repository への接続を行い、cloudbuild を指定します。
まとめ
とりあえず動きました。FastAPIを実践で使ったことがないのですが、これで何とか使えそうな気がします。
次は、
- 同じものをAWS Lambda で動かす
- CloudRun で認証をつける
ができたらいいかな。
(2022/05/05)