---
lang: ja-jp
tags: Infrastructure
---
# K8s Hands-on ①Backend APIのデプロイまで
[Hands-on: Index](https://hackmd.io/@moriaki3193/ByxAsfPAH)
:::success
**Keywords**
- Containerization(コンテナ化)
- Infrastructure as Code
- Kubernetes
- Kubernetes Resources
- Pod, ReplicaSet, Deployment, Service
:::
## Overview

## 顧客からの要望
- 花弁の長さやがく片の幅といった情報から、その花の品種を推定するアプリが欲しい
- 予測をWeb APIとして提供して欲しい ← ==イマココ==
- デモを用意して、ユーザに簡単に触ってもらえるようにしたい
## 作業の流れ
1. Backend APIを実装し、コンテナ化する(Mock APIを作成するイメージ)
2. KubernetesにDeploymentをApplyする
## Mock API 編
> Backend APIを実装し、コンテナ化する
*アヤメの品種分類問題*を解くにあたっての入出力は次のように要件定義されているとします。
- **入力**: 4種類の数値
- sepal length (cm) がく片の長さ
- sepal width (cm) がく片の幅
- petal length (cm) 花弁の長さ
- petal width (cm) 花弁の幅
- **出力**: 3品種のどれであるかの推定結果
### Overview
:::info
リクエスト`GET localhost:5000`を行うと`Hello, world!`というメッセージを含んだJSONを返すAPIを作成し、Dockerを利用してコンテナ化します。
このセクションは、Webアプリケーションをコンテナ化したことがない方々がコンテナ作成の作業をこなせるようになるために用意されているので、すでにその経験がある方々はスキップして次のセクションに進んでください。
:::
次のような構成でディレクトリとファイルを作成します。
```text=
.
├── README.md
├── backend
│ ├── server.py
│ └── uwsgi.ini
└── dockerfiles
└── backend.Dockerfile
```
### 【実装】 Flask Web server
[Flask](https://palletsprojects.com/p/flask/)を利用して、Web APIを作成します。
コンテナ化する際に、バージョン指定したものをまとめてコンテナ内にインストールできるように、パッケージマネージャを利用しましょう。
この例では[Pipenv](https://pipenv-ja.readthedocs.io/ja/translate-ja/advanced.html)を利用します。他のパッケージマネージャに詳しい方は、他のものを利用して後続のセクションの内容を適切に読み替えてください。
まずは、Flaskを利用可能な状態にします。
```shell=
$ pwd
# backendディレクトリに移動する
# >>> /path/to/$PROJECT_NAME/backend
# Pipenvの仮想環境を作成する
# Pythonのバージョンについてはローカルで手に入るものを利用しても良いです
$ pipenv --python 3.7.4
# Flaskのインストール
$ pipenv install Flask
```
次に、`server.py`の内容を次のようにしましょう。
```python=
"""Backend API server.
`app` is an WSGI-compatible application object.
"""
from http import HTTPStatus
from flask import Flask, jsonify
# Application object
app = Flask(__name__)
# Application routing
@app.route('/', methods=['GET'])
def index():
"""Hello, world!
"""
return jsonify({'message': 'Hello, world!'}), HTTPStatus.OK
if __name__ == '__main__':
app.run(debug=True)
```
ひとまず、ここまででアプリケーションが正しく動作するかどうかを確認してみましょう。
```shell=
$ pwd # /path/to/$PROJECT_NAME/backend
# for bash, zsh users
$ export FLASK_APP=server.py
# for fish users
$ set -x FLASK_APP server.py
$ pipenv run flask run
# アプリの起動: 127.0.0.1:5000 をブラウザなどで確認する
# >>> {'message': 'Hello, world!'}
```
### 【実装】 アプリケーションサーバのコンテナ化
Flaskのアプリケーションを定義したので、次に以下の2つのタスクをこなします。
1. uWSGIでサーバを起動できるようにする
2. アプリケーションをコンテナ化する
上述の`server.py`の`app`はWSGI互換のアプリケーションオブジェクトです。このオブジェクトをuWSGIから呼出すことでアプリケーションサーバを起動します。
アプリケーションサーバの設定を、`uwsgi.ini`ファイルに記述しましょう。
```ini=
[uwsgi]
master = true
module = server
callable = app
gid = uwsgi
uid = flask
python-path = /workspace
http-socket = :5000
```
次に、アプリケーションをコンテナ化していきます。`dockerfiles/backend.Dockerfile`を開き、次のように編集しましょう。
```Dockerfile=
FROM python:3.7-buster
LABEL maintainer="YOUR NAME <you@sample.com>"
WORKDIR /workspace
# Environment variables
ENV TZ="Asia/Tokyo"
# Add Pipfiles
COPY Pipfile Pipfile.lock ./
# Update the image
RUN apt-get update && apt-get upgrade -y
# Install package dependencies for this project
RUN pip3 install --upgrade pip \
&& pip3 install pipenv uwsgi \
&& pipenv install --deploy --system
# Add servers
COPY server.py uwsgi.ini ./
# Make a user running the application
RUN groupadd -g 10001 uwsgi \
&& useradd -u 10001 -g uwsgi flask
USER flask
# Run the application
CMD [ "uwsgi", "--ini", "/workspace/uwsgi.ini" ]
```
Dockerイメージをビルドしましょう。
```shell=
$ pwd # /path/to/$PROJECT_NAME
# 以下のコマンドをよく打ち込むことになるので、Makefileなどを作成して単純化して良いかもしれない
$ docker build -t local/k8s-hands-on:latest -f ./dockerfiles/backend.Dockerfile ./backend
```
:::info
`$ docker build`コマンドで作成されるイメージの実体(ファイル)のことをartifactと呼ぶようです
:::
ビルドされたイメージを利用して、コンテナを実際に立ち上げてみます。
```shell=
$ pwd # /path/to/$PROJECT_NAME
$ docker run --rm --name k8s-hands-on --publish 5000:5000 local/k8s-hands-on:latest
```
先程と同じように、`GET localhost:5000`をリクエストし、メッセージが取得できることを確認しましょう。
### 【解説】 コンテナ化して何が変わったのか
==WIP==
### 【実装】 Mock API endpointの追加
実際に機械学習モデルによる推論をAPI化することを見越して、Flaskによるサーバがモデルの入力を受け付けられるようにしましょう。
入力と出力は次のようなJSON形式でやり取りされることが仕様として定められていることを想定します。
```javascript=
// 入力
{
"sepalLength": <FLOAT>,
"sepalWidth": <FLOAT>,
"petalLength": <FLOAT>,
"petalWidth": <FLOAT>
}
// 出力
{
"result": <Enum{"setosa", "versicolor", "virginica"}>
}
```
まずは、`server.py`にエンドポイントを追加して、JSONを受け取り、なんらか結果を返すようにしてみましょう。
```python=
import random # ← 追加
from flask import Flask, jsonify, request # ← `request`を追加
# index()の次あたりに追加する
@app.route('/api/estimate')
def iris():
"""Make model inference
"""
payload = request.get_json()
# TODO Data preprocessing
# TODO Model prediction
# TODO Remove below
candidates = ('setosa', 'versicolor', 'virginica')
idx = random.randint(0, 2)
return jsonify({'result': candidates[idx]}
```
再度イメージをビルドして、`POST localhost:5000/api/estimate`リクエストを叩いて見ましょう。POSTリクエストを作成するには、[Postman](https://www.getpostman.com)が便利です。`curl`などのコマンドを利用する場合は、リクエスト内容は次のようになります。
```shell=
$ curl -H 'Content-Type:application/json' -d '{"sepalLength": 1, "sepalWidth": 2, "petalLength": 3, "petalWidth": 4}' http://localhost:5000/api/estimate
```
何度か繰り返しリクエストをしてみると、結果が変わることが見て取れると思います。現在はランダムな結果を返していますが、この部分のロジックが学習済みのモデルによる予測結果に置き換わるイメージです。
ここまでで、とりあえず振る舞いだけは「それっぽい」Web APIを作成できました。次はこのなんちゃってAPIを世界に公開します。
## K8s Deployment 編
[Kubernetes Engine](https://cloud.google.com/kubernetes-engine/?hl=ja)を利用して、アプリケーションのデプロイを遂行していきます。
なんらかのGCPのプロジェクトを作成し、Kubernetes EngineのAPIを有効化してください。また、Cloud SDKのインストールが必要になります。
### 【運用】 クラスタのセットアップ
```shell=
# kubectlのインストール(GCP SDKが利用できることを想定)
$ gcloud components install kubectl
# 新しい設定を追加する(既存の設定に影響を与えないため)
## 設定を新規作成
$ gcloud config configurations create $CONFIG_NAME
## 作成した設定がアクティブになっているかどうかの確認
$ gcloud config configurations list
## リソースの場所やアカウント情報
$ gcloud config set compute/region us-central1 # 好きな
$ gcloud config set compute/zone us-central1-b # 場所で良い
$ gcloud config set core/account $YOUR_EMAIL_ADDRESS
$ gcloud config set core/project $PROJECT_ID
# ノード数が1のクラスタを作成する
# Kubernetes Engine APIの有効化が必要
$ gcloud container clusters create k8s-hands-on-cluster --num-nodes=1
```
マニフェストファイルを書き始める前に、GCRにPushするイメージを作成しておきます。先程との差分は、タグの名称を変更している点です。
```shell=
$ docker build -t gcr.io/$PROJECT_ID/k8s-hands-on-backend:v1 -f ./dockerfiles/backend.Dockerfile ./backend
```
GCRにアーティファクトをアップロードしましょう。
```shell=
gcloud docker -- push gcr.io/$PROJECT_ID/k8s-hands-on-backend:v1
```
コマンドが正常に終了するのを確認したら、GCPコンソールからContainer Registryのサービスに移動し、アーティファクトが正しくアップロードされていることを確認しましょう。
### 【実装】 最初のマニフェストファイル
プロジェクトのルートディレクトリに`manifests`という名前のディレクトリを作成し、その中に`Deployment.yaml`と`Service.yaml`という名称のファイルを作成してください。
このHands-onを順番に読み進めてきた場合には、ディレクトリ構成は次のようになっているはずです。
```text=
.
├── README.md
├── backend
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── server.py
│ └── uwsgi.ini
├── dockerfiles
│ └── backend.Dockerfile
└── manifests
├── Deployment.yaml # New!!
└── Service.yaml # New!!
```
#### Deployment.yaml
==WIP== Deploymentリソースとはなんぞやの説明
```yaml=
# ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-hands-on-deployment
labels:
app: k8s-hands-on
spec:
replicas: 1
selector:
matchLabels:
app: k8s-hands-on-pod
template:
metadata:
labels:
app: k8s-hands-on-pod
spec:
containers:
- name: k8s-hands-on-backend
image: gcr.io/$PROJECT_ID/k8s-hands-on-backend:v1
ports:
- containerPort: 5000
```
マニフェストファイルの記述が終わったら、以下のコマンドを実行してクラスタにリソースを展開してみましょう。
```shell=
$ kubectl apply -f ./manifests/Deployment.yaml --record
# optional: 展開されたリソースを確認
$ kubectl get deployments
$ kubectl get replicasets
$ kubectl get pods
```
##### Trouble Shooting
- `$ kubectl get pods`コマンドを打ってみても、Podの状態が`Running`にならない
- `spec.template.containers[].image`の値が間違ってはいませんか?
- GCRにイメージは正しくアップロードされていますか?
#### Service.yaml
==WIP== Serviceリソースとはなんぞやの説明とDeploymentとの関係性
```yaml=
# https://kubernetes.io/ja/docs/concepts/services-networking/service/
apiVersion: v1
kind: Service
metadata:
name: k8s-hands-on-service
labels:
app: k8s-hands-on
spec:
type: LoadBalancer
ports:
- name: backend-port
port: 80
targetPort: 5000
protocol: TCP
selector:
app: k8s-hands-on-pod
```
マニフェストファイルの記述が終わったら、以下のコマンドを実行してクラスタにリソースを展開してみましょう。
```shell=
$ kubectl apply -f ./manifests/Service.yaml --record
```
また、展開されたServiceリソースについて、状態を確認し、公開されているIPアドレスを取得してみましょう。
```shell=
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
k8s-hands-on-service LoadBalancer 10.43.252.xxx aaa.bbb.ccc.ddd 80:30597/TCP 2m5s
kubernetes ClusterIP 10.43.240.y <none> 443/TCP 30m
```
上の例では、`aaa.bbb.ccc.ddd`となっています(実際にはなんらか意味のある数値が入っています)。このIPアドレスを利用して、ローカルで確認したWeb APIの挙動を確認してみましょう。以下のような結果が得られれば、正しくデプロイされていると言えます。
```shell=
$ curl aaa.bbb.ccc.ddd
{"message":"Hello, world!"}
$ curl -H 'Content-Type:application/json' -d '{"sepalLength": 1, "sepalWidth": 2, "petalLength": 3, "petalWidth": 4}' http://aaa.bbb.ccc.ddd/api/estimate
{"result":"virginica"}
```
## まとめ
このチュートリアルでは、次のようなことを学びました。
- アプリケーションのコンテナ化
- GCPを利用したKubernetesクラスタのセットアップ
- インフラ構成のコード化(Infrastructure as Code)
- **KubernetesのDeployment, Serviceリソースの具体例**
次回の[②Frontendとの連携まで](https://hackmd.io/@moriaki3193/HJ273MPAS)では、ユーザにデモを確認してもらうためのWeb Frontendアプリを開発し、Backend APIと同一のPodに含めた公開を行います。この際に、FrontendないしBackendのどちらのサーバにアクセスを捌けば良いのかの設定が必要になります。OSI基本参照モデルでいうL7のアプリケーションレイヤでのルーティング機能として、Kubernetesの`Ingress`リソースを利用した解決方法を紹介する予定です。