アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

GKE上のkubernetesで機械学習運用環境(MLops)を作成手順・コマンド・知識メモ

機械学習運用環境(MLops)の一部を話題のkubernetes(k8s)で作成してみた。GCPのGKE上でk8sを利用できるので、今回はGKE上で途中まで構築。

今回参考にするMLopsは下の図。どうもこのサイトによるとメルカリでマイクロサービスとして運用されてるらしい  




このMLopsのk8sの部分だけ(下図)作成してみた。




・作成手順
step1. persistentVolumeを使ってAWSのS3とマウントしたPodをLoad balancerを使って、負荷分散を行う。http://[EXTERNAL-IP]にアクセスして、flaskで’hello, world’を表示させる。


step2.redisインスタンスと結合させたflaskアプリケーション用Podをgoogleチュートリアルに従い、作成


step3.上の図のPersistentVolumeと繋がってるtensorflowの部分の「worker用Pod」を作成。今回は簡易版でworker用のpodをceleryで作成



途中からAmazon sageMakerの方がMLopsを簡単に作れるという噂で、k8sでの構築はstep3で中断。けど、せっかくなのでアウトプットしたログを記事にまとめておこうと思う。

構築に使用したコードとかファイルはgithubにあげてあリます。

目次
・MLopsの構築

kubernetes関係知識まとめ

・コマンド関連まとめ



MLopsの構築

step1. flask用Podを作成して、で’hello world'を表示

まずflaskアプリケーションで’hello world’を表示させてみる。flaskはDjangoより便利ってことで使った。


・用意したファイル
app.yaml 、main.py(flaskアプリ本体) 、requirements.txt(インストール用のライブラリ)、Dockerfile、flask_pod.yaml(Pod作成用マニフェストファイル)、sample_lb.yaml(load balancer用ファイル)


プロジェクト ID:sturdy-willow-167902


1. Dockerイメージpush & 確認

まずGCPのホーム画面からcloud shell(コンソール)を開いて(右上のとこにある)、kubernetes Engineを選択。 

cloud shell(コンソール)の起動方法は、GCPメイン画面から[プロジェクト]をクリック。

上段の右から6番目のアイコン「cloud shellにアクセス」をクリックし、「起動」で開ける。





以下コマンドはコンソール上で実行。

# Dockerイメージ作成して、Container Repositoriesにpush
$ export PROJECT_ID="$(gcloud config get-value project -q)”
$ docker build -t gcr.io/${PROJECT_ID}/visit-counter:v1 .
$ gcloud docker -- push gcr.io/${PROJECT_ID}/visit-counter:v1

#コンソール右上の [ウェブでプレビュー]で ’hello world’ が表示されるか確認
$ docker run --rm -p 8080:8080 gcr.io/${PROJECT_ID}/visit-counter:v1

→ Container Repositoriesに「visit-counter」ができる



2.Podの作成とserviceの定義


ここは単純にkubektlコマンドでPodの作成とサービスの定義(負荷分散)だけ。

そのためにGUIでもCUIでもいいので、kubernetes Engineのクラスタを作成しておく。クラスタを作成したら、コンソールで以下コマンド実行。

# クラスタ操作権限取得
$ gcloud container clusters get-credentials <クラスタ名> --zone=asia-northeast1-a

# 一応、VMインスタンス確認
$ gcloud compute instances list


***注意、マニフェストファイル(flask_pod.yaml)ではイメージ名のプロジェクトIDを展開する参考サイト

asia.gcr.io/$PROJECT_ID/wdpress/hello-world
# $PROJECT_ID は展開する必要があ流ので、上のイメージ名を下のように修正
asia.gcr.io/sturdy-willow-167902/wdpress-123456/hello-world

# Podの作成
$ kubectl apply -f flask_pod.yaml (pod確認  $ kubectl get pod)
# Serviceの定義
$ kubectl apply -f sample_lb.yaml
# EXTERNAL-IP(IPアドレス)の確認
$ kubectl get service

→ http://[EXTERNAL-IP] にブラウザからアクセスして、’hello world’が表示される。



step2.redisインスタンスと結合したflask用Pod作成

redisインスタンスと結合したflaskアプリケーションのPodを作成。
作成するMLopsではこの部分

これはGoogleのチュートリアルがあるが、素でやってもすんなり行かないので、ちょっと修正した。あとはチュートリアル通りに実行。 


チュートリアルで修正した点
インスタンスクラスタのリージョンを同じにする => region=us-central1

・RESERVED_IP_RANGE の部分を、実際のインスタンスの予約済み IP 範囲に置き換える(参考サイト

マニフェストファイル(YAMファイル)のイメージ名のプロジェクトIDを展開



この3点だけ修正。あとはチュートリアル通りに実行。

プロジェクトID:sturdy-willow-167902



1.redisインスタンス作成

# インスタンス作成
$ gcloud redis instances create myinstance --size=2 --region=us-central1

# インスタンス情報確認
$ gcloud redis instances describe myinstance --region=us-central1

>> createTime: '2019-01-06T13:19:26.236748318Z'
currentLocationId: us-central1-c
host: 10.0.0.3
locationId: us-central1-c
memorySizeGb: 2
name: projects/sturdy-willow-167902/locations/us-central1/instances/myinstance
port: 6379
redisVersion: REDIS_3_2
reservedIpRange: 10.0.0.0/29
state: READY
tier: BASIC

# redisインスタンスを削除するとき
$ gcloud redis instances delete myinstance ―region=us-central1




2.CUIクラスタ作成

#  redisインスタンスのリージョンに設定
$ gcloud config set core/project sturdy-willow-167902 && gcloud config set compute/zone us-central1-c
# クラスタ作成
$ gcloud container clusters create visitcount-cluster --num-nodes=3 --enable-ip-alias
# クラスタの権限get
$ gcloud container clusters get-credentials visitcount-cluster --zone us-central1-c --project sturdy-willow-167902





3.RESERVED_IP_RANGE の部分を、実際のインスタンスの予約済み IP 範囲に置き換え

$ git clone https://github.com/bowei/k8s-custom-iptables.git && cd k8s-custom-iptables/

$ TARGETS="10.0.0.0/29 192.168.0.0/16" ./install.sh && cd ..

4.Dockerイメージ作成とpush

$ export PROJECT_ID="$(gcloud config get-value project -q)"
$ docker build -t gcr.io/${PROJECT_ID}/visit-counter:v1 .
$ gcloud docker -- push gcr.io/${PROJECT_ID}/visit-counter:v1

5. configmapの作成

$ kubectl create configmap redishost --from-literal=REDISHOST=10.0.0.3
# configmap確認
$ kubectl get configmaps redishost -o yaml


6.pod作成, service定義、アクセス

$ kubectl apply -f visit-counter.yaml
$ kubectl get service visit-counter

# アクセスして動作確認
$ curl http://[EXTERNAL-IP]



Step3.celeryでworker用のPodを作成

ここでは下図のworkerのPodを作成。

今回はPVと接続せず、簡単にworker用のcelery のpodを作成してみた。
用意したfileは「Dockerfile, run.sh, celery_conf.py, celery_worker.yaml」。


そのためには別途dockerイメージを作成する必要がある。

$export PROJECT_ID="$(gcloud config get-value project -q)"
$ docker build -t gcr.io/${PROJECT_ID}/visit-counter:worker .
$gcloud docker -- push gcr.io/${PROJECT_ID}/visit-counter:worker

各ファイルのコードは以下の通り。
 
・Dockerfile

FROM library/celery
ADD celery_conf.py /app/celery_conf.py
ADD run.sh /usr/local/bin/run.sh

ENV C_FORCE_ROOT 1
CMD ["/bin/bash", "/usr/local/bin/run.sh"]      # run.shのコマンド実行
CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait"  # 永続的に起動される


・run.sh

#!/bin/bash
# celery worker起動コマンド
/usr/local/bin/celery -A celery_conf worker -l info


celery_conf.py

from celery import Celery
from flask import Flask

def make_celery(app):
    celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'],
                    broker=app.config['CELERY_BROKER_URL'])
    celery.conf.update(app.config)
    return celery

flask_app = Flask(__name__)
flask_app.config.update(CELERY_BROKER_URL='redis://10.0.0.3:6379', CELERY_RESULT_BACKEND='redis://10.0.0.3:6379')
celery = make_celery(flask_app)

@celery.task()
def add(a, b):
    return a + b


celery_worker.yaml
参考サイトによるとspec.template.metadata.labelsをvisit-counterにすることでflask側のPodと接続させるらいし

apiVersion: v1
kind: ReplicationController
metadata:
  labels:
    component: celery
  name: celery-controllers
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: visit-counter
        component: visit-counter
    spec:
      containers:
      - image: "gcr.io/sturdy-willow-167902/visit-counter:worker"
        name: celery 
        env:
        - name: REDISHOST
          valueFrom:
            configMapKeyRef:
              name: redishost
              key: REDISHOST
        ports:
        - containerPort: 5672
        resources:
          limits:
            cpu: 100m


最終的に作ったPodが

$ kubectl apply -f visit-counter.yaml && kubectl get pod 

で確認したとき、「Running」になっているので、worker用podの完成。


あとは、flask側のpodで

$ from celery_conf import add
$ r=add.delay(1,3)
$ r.result

こんな感じでworker側に投げた値を受け取れるようにする必要がある。
これはserviceを定義するとか、マニフェストファイルを定義するとか、いろいろありそうなので、またの機会にした。

というのも、amazon sagemakerというのが便利との噂を聞いて、k8sからamazon sagemakerに切り替えてMLopsを構築したから。
なので、k8sでの作業は中断し、amazon sagemakerでmlopsを構築した。


ちなみに、Amazon sageMakerでのMLops構築手順の関係記事は別記事にまとめた。

trafalbad.hatenadiary.jp




kubernetes関係知識まとめ

PersistentVolumeについて

k8sのpersistentvolume (以下PV)はPodとは独立した外部ストレージ。

通常のk8sのPVはクラスタの外部の別の場所に作成するが、GCP上ではPVは代替ストレージの「GCEPesistentDisk」がすでに存在してる。PVCで用途に応じて容量・タイプなどを適宜追加・拡張する。




・PVのアクセスモード

  • > ReadWriteOnce: 1つのノードからR/Wでマウントできる
  • > ReadOnlyMany: 複数のノードからReadOnlyでマウントできる
  • > ReadWriteMany: 複数のノードからR/Wでマウントできる


・PVCの作成時、PVのストレージタイプの「標準」か「SSD」をStorageClassで指定

SSD」はマニフェストファイルではstorageClassName: ssdで指定。デフォルトは「標準」



app.yamlPython ランタイムについて

=>アプリケーションのコードと依存関係をインストールして実行するソフトウェアスタック。
標準ランタイムは、app.yaml で以下のように宣言(Docker でビルド)
runtime: python
env: flex


=>アプリケーションの app.yaml で使用するバージョン(Python2 or 3)をruntime_config に指定

runtime: python
env: flex
runtime_config:
    python_version: 3

=>ライブラリのインストール
同じディレクトリ内の requirements.txt ファイルを検索し、pipでrequirements.txt 内のライブラリを全てインストールする。




・PV<=> PVC <=> Pod間の結合関係
PVとPVCとPod間のどこが結合してるか?の関係について、


PV:metadata.name: nfs001, storageClassNameフィールドのslow

PVC:metadata.name: nfs-claim1, storageClassNameフィールドのslow

Pod:metadata.name: my-nginx-pod, persistentVolumeClaim.claimNameフィールドのnfs-claim1

つまりPVとPod、PVCとPodの結合関係は
PVとPod: slow(PV) <=> slow(Pod)
PVCとPod:nfs-claim1(PVC)<=> nfs-claim1(Pod)


DockerfileのENVの環境変数について
ENV命令は、環境変数と値のセット。値はDockerfile から派生する全てのコマンド環境で利用でき、 上書き、変数扱いも可能。

ex: 環境変数に値をセットする場合、

ENV MES Good-bye 

は MES=Good-bye
を意味する。 $MESで環境変数をあらわせる。




・gunicorn
例えば myapp.py というファイル内に app という名前でFlaskアプリがある場合、以下のように gunicorn で起動。

$ gunicorn myapp:app

デフォルトのPORTとHOSTは127.0.0.1と8000



gunicornのportについて
This will start one process running one thread listening on 127.0.0.1:8000.

I wish to run gunicorn on port 80.
→ gunicorn -b 0.0.0.0:80 backend:app




・flask

if __name__ == ‘__main__':
   app.run(debug=False, host='0.0.0.0', port=80)

host='0.0.0.0' の指定が大事。これがなければ、外部からアクセスすることができない。参考サイト



・os.environ.get
環境変数 key が存在すればその値を返し、存在しなければ default を返す。
key、default、および返り値は文字列。
もし環境変数がなくともKeyErrorがraiseされず、第二引数(キーワード引数)のdefaultの値が返る。(指定しなければNoneが返る)



・PORT、HOST
REDISHOST→ネットワークに接続されたコンピュータのこと(redisインスタンス自体)
REDISPORT→redisインスタンスに接続するドア(番号)




・configMap
# マニフェストファイル内での部分

env:
     - name: REDISHOST
        valueFrom:
          configMapKeyRef:
            name: redishost
            key: REDISHOST

keyのREDISHOSTが環境変数

# configmap作成コマンド
$ kubectl create configmap <map-name> <data-source>

# 例
$ kubectl create configmap redishost ―from-literal=REDISHOST=10.0.0.3

redishostがmap-nameで、それ以下がdata-source

# configmap削除
$ kubectl delete configmap <configmap名>

上のコマンドで環境変数のREDISHOSTが10.0.0.3に変更された。

REDISHOST=redisインスタンス自体を環境変数に代入。Dockerfileのデフォルトのクラスタ環境変数をredisインスタンスのPORT(自体)で上書き。




コマンド関連まとめ



(ターミナル上での)k8sクラスタ作成コマンド

$ gcloud container clusters create <クラスタ名> --machine-type=n1-standard-1 ―num-nodes=3

ex: gcloud container clusters create mlops --machine-type=n1-standard-1 --num-nodes=3



クラスタ環境設定の権限取得(cloud shell上で作成したクラスタを操作できる)

$ gcloud container clusters get-credentials <名前> ―zone=<ゾーン>

ex:$ gcloud container clusters get-credentials mlopss --zone=asia-northeast1-a



kubectlコマンドでクラスタ内のノードとpod関係の操作

$ kubectl get nodes (ノードの確認)
$ kubectl get pod  (Podの確認)
# Serviceの情報を確認(クラスタ内で通信可能なIPアドレス)
$ kubectl get services sample-clusterip

# Pod内へのログイン
$ kubectl exec -it sample-pod /bin/bash
→sample-podはmetadata.nameフィールド名



・dockerコマンド関係

$ docker rmi <イメージID>  -f (dockerイメージの削除) 
$ docker images (dockerイメージ確認)
$ docker ps(コンテナが正常に起動しているか確認)
$ docker images -aq | xargs docker rmi(dockerイメージ全部削除)

# コンテナ停止、削除
$ docker stop [コンテナ名]  (全コンテナ停止 docker stop $(docker ps -q))
$ docker rm [コンテ名] (全コンテナ削除 docker rm $(docker ps -q -a))