機械学習運用環境(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で中断。けど、せっかくなのでアウトプットしたログを記事にまとめておこうと思う。
目次
・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構築手順の関係記事は別記事にまとめた。
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.yaml のPython ランタイムについて
=>アプリケーションのコードと依存関係をインストールして実行するソフトウェアスタック。
標準ランタイムは、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間のどこが結合してるか?の関係について、
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(自体)で上書き。
コマンド関連まとめ
$ 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))