CircleCI2.0からGAEスタンダード環境のruntime:python3.7へデプロイする手順

概要

GAEのスタンダード環境のpython3.7を使う際に、CircleCIから直接デプロイできるようにした。CircleCIがDockerコンテナベースでのビルドなので、そこからgcloud SDKでデプロイするための設定をすればOK。基本的にpython3でAppを作ってるけど、gcloud SDKがpython2で動くので共存させる必要がある。

今回はpython2、python3、Google Cloud SDKを入れてあるDockerイメージを用意し、CircleCIの.circleci/config.ymlの中で呼び出し、その中でgcloudコマンドを叩いてデプロイするようにしました。gcloudでデプロイするためにcircleciで立ち上げたコンテナの中でサービスアカウントを認証しなければならない点です。

Docker image

Python3.7-strechのイメージに、python2.7、Google Cloud SDKが入ったDockerイメージを自作しました。下記のDockerhubのリポジトリに入っていますので、必要に応じてCircleCIで使うなりpulsるなりlして使って下さい。 Dockerfileの入ったリポジトリは下記です。最初pip3がうまく起動しなかったので、gcloudSDK, python2を入れた後に再度pip3を入れてる必要があったのが辛かった。

github.com

CircleCIの設定

CircleCIからgcloud SDKを使ってデプロイする際にはサービスアカウントを認証する必要があります。サービスアカウントの認証方法は、下記3つありますが、CircleCI上からはブラウザを使った認証が出来ないので、3を使います。

  1. gcloud init を使う
  2. gcloud auth login を使う
  3. cloud auth activate-service-account --key-file を使う

この3つ目の認証には、

  • サービスアカウントのメールアドレス
  • サービスアカウントの認証キー
  • サービスID

3つを用意しなければいけません。 サービスアカウントはGCPのコンソールからIAMタブへいき、アドレスと認証キーを入手し、手元に控えて下さい。 このアドレスと認証キーをconfig.yamlに直接書いたりするのは管理面で怖いです。よってCircleCIのアプリの中でEnvironment Variablesというconfig.ymlに環境変数を渡す機能があるので、それを使って渡すことになります。CircleCIアプリの中で個別のプロジェクトのPROJECT SETTINGSで設定しましょう。認証キーはJSONの中身をそのままコピペするだけで大丈夫です。

設定が完了したら.circleci/config.yamlを書きます。関連する部分だけを書き出すと下記のようになります。

version: 2
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: tkazusa/appengine-python37:latest
    steps:
      - checkout

      - run:
          name: install dependencies
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install -r requirements.txt
        
      - run:
          name: google auth
          command: |
            echo $DEV_SERVICE_ACCOUNT_KEY > /tmp/secret.json
            gcloud auth activate-service-account $DEV_SERVICE_ACCOUNT_CLIENT_EMAIL --key-file /tmp/secret.json
      - run:
          name: deploy production
          command: |
            gcloud --quiet config set project $GOOGLE_PROJECT_ID
            gcloud --quiet app deploy app/app.yaml --project $GOOGLE_PROJECT_ID

まとめ

Dockerコンテナの準備がややめんどくさかったですけど、これで個人でもCircleCIからGAEにCDできるようになりました。DockerhubのAutobuildも勉強できて良かった。

参考

機械学習応用システム(MLS)パターン勉強会に参加してきた

概要

機械学習工学研究会(MLSE)の機械学習応用システム(MLS)パターン勉強会に参加してきました。実例や既に公開されているパターンをベースに機械学習システムにおける、デザインパターン、アンチパターン、プロセスパターン、組織のパターンについて議論することで機械学習システムの開発のプロセスやアーキテクチャにパターンを見つけ、名前をつけ、熟練者のノウハウや思考法を横展開できるようにすることが会の目的となっていました。

mlxse.connpass.com

19:00-19:20 パターンとは?:鷲崎教授(早稲田大学)
19:20-19:30 チーム編成
19:30-20:30 チームごとにパターンの議論
20:30-21:00 発表
21:00-21:30 議論&懇親

実際にやったこと

パターンについてのガイダンス

勉強会冒頭でパターンの権威である鷲崎教授(早稲田大学)にガイダンスをいただきました。もともと建築の世界で使われていたパタン及びパタンランゲージにオブジェクト指向なソフトウェア業界が取り入れていったというパタンの歴史的な経緯や、その見つけ方などをさらっと20分。ソフトウェア開発におけるデザインパターンなんかは、経験的に良い繰り返しパターンを実践に持ち込んだ形だし、アジャイル開発が「繰り返しを見つけていこう」というマイニングの手続き活動が流行らなかったことを反映して、「こういうやり方でいきましょう」と割り切って決めたひとつのプロセスパタンの形という話は面白かった。

パタンについてもう少し

熟練者のノウハウを横展するためにパタンを探し出して名前をつける。そのMinimalとしては「こういうとき(文脈)に、こういうことで困る(課題)ので、こうしよう(解決策)」で成り立つが、展開する際に意図や適用法が正しく伝わらないようなことが起こり得るので、もう少し詳細に下記のような項目で検討するらしい。 - 状況(文脈) - 問題 - フォース(考慮するべき点) - 解決策 - 結果

フォースの概念が難しいが、文脈における制約事項や懸念事項だと考えれば良いっぽい。

チームごとにパターンの議論

数名ずつ下記の4つのテーマについて議論するチームに分かれてパタンを洗い出しまとめる作業を実施しました。

  • データ取得、加工、管理
  • 組織、マネジメント、プロセス
  • 要求、設計、CHI
  • テスト、検証、運用

色々アウトプットが出てきたのですが、その中で面白いなと思ったパタンが下記です。PFNさんとDeNAさんで同じようなパタンで業務を行っていたとのことで妥当性があるんだろうなと。自分の業務の中で考えても納得感がある。

  • MLアルゴリズム開発とインフラ分離のパターン
    • 文脈:機械学習には複雑なインフラが必要になりがち
    • 課題:MLエンジニアやリサーチャがアルゴリズム開発に集中できない
      • インフラやアノテーションツールを開発して、時間が足りなくなる
    • フォース:インフラ、アノテ、アルゴリズムは専門性が異なりかつ疎結合にできる
    • 解決法:チームをある程度分離してしう
    • 結果:アルゴリズム開発者が主業務に集中できる

感想

そろそろAIブームも落ち着いてきて、普通の技術として普及期に入ってきているので共通言語を持って話せば、もっともっと社会実装が進むだろうなと感じた。各所で勉強会たくさんやってるけど、その内容まとめて整理・編集してパタンに名前をつけるような作業はどこでやればいいんだろう。Web業界との親和性高いからか、いろんなところで発表された内容が消費されてるだけ感あるしな、ここんところ。

【書評】プログラムはなぜ動くのか 第2版

中身の概要

データ分析のためにプログラミングを使う立場で計算機と付き合ってきたが、機会学習モデルをシステムとして本番環境にデプロイするようになってくると、コンピューターのこと知らなさ過ぎることの辛みが出てきたので、今年は計算機や情報工学についてもう少し詳しくなる目的で勉強を始めました。その第一弾がプログラムはなぜ動くのか 第2版です。

プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識

プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識

内容は、

  • CPUの仕組み
  • データの2進数での取り扱い方
  • 浮動小数点の扱い
  • メモリーの仕組み
  • コンパイルとアセンブリ
  • ハードウェアの制御

のようになっており、コンピューターをなんかよくわからんが計算してくれる箱だと思っている自分としてはこれがきっと基礎なんだろうなという内容が並んでいる。

解説のために出てくるCのコードをGoに置き換えながら読もうかと思いましたが、意外とC言語のコード少なく特別なこともしてなかったので、自身としてはあんまり効果なかったかなって感じです。一応リポジトリにコード置いて少しづつ進めます。

良かった点

特にメモリの仕組みや、コンパイル、アセンブリ言語のあたりはpythonでのデータ分析や機械学習モデリングをメインで取り扱っていると隠遁されていて意識していなかった部分だったので多少イメージがつくようになりました。 特に10章のC言語とアセンブリ言語を1対1で対応付けて解釈していく部分は、プログラミング言語の仕組みが腹に落ちた気がして、LLじゃなくても怖くないやん!となれたのはでかい。システムプログラミングをやろうと年末年始にC言語の勉強をしていたのはまじでためになっている気がする。CとGoそれぞれから作られたアセンブリの比較とかしたいな。時間あるときにやる。

会社のエンジニアも言ってたけど、この類の低レベルの話はすぐに何かが出来るようになるわけでもなく、誰かに褒められるわけでもないが、とはいえ知らないまんまだと新しい技術へのキャッチアップが進まんくなるというのはすごく納得感あるので継続して勉強していきたい。

次はGoならわかるシステムプログラミングやります。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

【Kubeflowのラズパイ包み】ラズパイにKubeflowをインストールして機械学習基盤化するとこまで

前回作ったkubernetesのラズパイ包みを、Kubeflowのラズパイ包みにして、機械学習基盤のおもちゃにする。

tkzs.hatenablog.com

まずはksonnetからのインストールから

ksonnetはkubernetesをjsonnetというJSON用のDSLを使った設定ファイル管理ツールっぽい。これを使ってKubeflowのデプロイなどの設定を行う。

Home ⋅ ksonnet

macOSならbrewで入れられるのだが、それ以外はバイナリからのビルドになる。

github.com

インストールにgolang必要そうなので、まずはそっちインストールから。

$ wget https://golang.org/dl/go1.10.1.linux-armv6l.tar.gz
$ sudo tar -C /usr/local -xzf go1.10.1.linux-armv6l.tar.gz
$ ls -l /usr/local/go
$ cat /usr/local/go/VERSION
$ sudo vi ~/.bashrc
$ source $HOME/.bashrc

次にksonnet。GitHubのリポジトリからバイナリを取ってきて、インストールし、パスを通す。

# Clone the ksonnet repo into your GOPATH
$ go get github.com/ksonnet/ksonnet

# Build and install binary under shortname `ks` into $GOPATH/bin
$ cd $GOPATH/src/github.com/ksonnet/ksonnet
$ make install
$ PATH=$PATH:$GOPATH/bin
$ ks version
ksonnet version: dev-2018-07-20T09:11:08+0000
jsonnet version: v0.10.0
client-go version: kubernetes-1.10.4

とりあえず入った。

ksコマンドは下記わかってればなんとかなる。

  • ks generate : componentsについてのmanifestを作成
  • ks apply : クラスタに実行可能なmanifestをあてる

ここから先はKubeflow v0.1のアナウンスを参考にする。

kubernetes.io

$ NAMESPACE=kubeflow
$ kubectl create namespace ${NAMESPACE}
$ VERSION=v0.1.3

# Initialize a ksonnet app. Set the namespace for it's default environment.
$ APP_NAME=my-kubeflow
$ ks init ${APP_NAME}
INFO Using context "kubernetes-admin@kubernetes" from kubeconfig file "/home/pi/.kube/config"
INFO Creating environment "default" with namespace "default", pointing to "version:v1.9.9" cluster at address "https://192.168.13.2:6443"
INFO Generating ksonnet-lib data at path '/home/pi/my-kubeflow/lib/ksonnet-lib/v1.9.9'

$ cd ${APP_NAME}
$ ks env set default --namespace ${NAMESPACE}

# Install Kubeflow components
$ ks registry add kubeflow github.com/kubeflow/kubeflow/tree/${VERSION}/kubeflow
$ ks pkg install kubeflow/core@${VERSION}
INFO Retrieved 22 files
$ ks pkg install kubeflow/tf-serving@${VERSION}
INFO Retrieved 5 files
$ ks pkg install kubeflow/tf-job@${VERSION}
INFO Retrieved 5 files

# Create templates for core components
$ ks generate kubeflow-core kubeflow-core
INFO Writing component at '/home/pi/my-kubeflow/components/kubeflow-core.jsonnet'

# Deploy Kubeflow
$ ks apply default -c kubeflow-core 
ERROR handle object: patching object from cluster: merging object with existing state: Get https://192.168.13.2:6443/api/v1/namespaces/kubeflow/configmaps/jupyterhub-config: net/http: TLS handshake timeout

最後のks applyでエラー吐くので、issueからWork aroundを見つけて対応。

github.com

$ ks show default -c kubeflow-core | kubectl apply -f 
service "k8s-dashboard" created
deployment "ambassador" created
service "ambassador" created
serviceaccount "ambassador" created
role "ambassador" created
rolebinding "ambassador" created
service "ambassador-admin" created
clusterrole "tf-job-dashboard" created
clusterrolebinding "tf-job-dashboard" created
clusterrole "tf-job-operator" created
clusterrolebinding "tf-job-operator" created
serviceaccount "jupyter-hub" created
role "jupyter-role" created
rolebinding "jupyter-role" created
configmap "jupyterhub-config" created
deployment "tf-job-dashboard" created
configmap "tf-job-operator-config" created
customresourcedefinition "tfjobs.kubeflow.org" created
deployment "tf-job-operator" created
statefulset "tf-hub" created
service "tf-hub-0" created
service "tf-hub-lb" created
service "tf-job-dashboard" created
serviceaccount "tf-job-dashboard" created
serviceaccount "tf-job-operator" created

眠すぎて、一旦とりあえず今日はここまで。モデルのServingとかは持ち越し。

events.linuxfoundation.org

KubeCon + CloudNativeCon EU 2018のセッションの資料を見ると、TF servingじゃなくてSeldon使えたり、TFじゃなくてsklearn使えるようになるっぽい。もう出来るようになっている?ぼちぼち試していく。

処理遅いしGKEでやりゃ良くね?みたいなことはコマンド打つ毎に考えるけど、それ言い出したらせっかくのラズパイ包み楽しくないから多少のことは我慢する。

参考

【Kubeのラズパイ包み】Rasberry Piを使ったKubernetes クラスタでの機械学習基盤構築

はじめに

機械学習基盤、もうちょいなんとかしたくて、勉強のためにKubernetesのオライリー本を買ったら、付録に「Rasberry Piを使ったKubernetes クラスタ構築」を見つけたので本体読み始める前に必要なもの揃えて構築してみました。この上で勉強すればいいかなって。

入門 Kubernetes

入門 Kubernetes

  • 作者: Kelsey Hightower,Brendan Burns,Joe Beda,松浦隼人
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/03/22
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

で、色んな方が既に実践されているので多いに参考にさせて頂きました。ありがとうございました。

developers.cyberagent.co.jp

qiita.com

色んな所でこのラズパイのkubernetes包みは紹介されているので、わざわざ記事にしなくても良いかなと思ったけど、そのままやったんじゃエラー吐いたから備忘録的に。

必要なモノ揃えて、 f:id:tkzs:20180720011759j:plain

組み立てる。

f:id:tkzs:20180720011841j:plain

機械学習基盤にしようとしてるけど、無線で全部つなげるっていう不安定さ、ホンマにいいんかなって気はするけど、まぁいいや。使ったRaspbianは下記。

$ lsb_relsease -a
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 9.4 (stretch)
Release:        9.4
Codename:       stretch

もろもろ準備

下ごしらえで3台のラズパイに下記を実施。

まずはネットワーク周りを確保したいが、 /etc/wpa_supplicant/wpa_supplicant.confを編集してWiFi有効化する方法だとなぜか反映されなかったので、

sudo raspi-config

でWiFi環境を設定しておいた。

ラズパイホスト名が同じraspberryになっていたのでk8s-master,k8s-node1, k8s-node2へ変更。

$ sudo apt-get update
$ sudo apt-get -y upgarade

$ sudo vi /etc/hosts
$ sudo vi /etc/hostname

さらにcgroupsの有効化するために。

$ cd /Volumes/boot
$ vi cmdline.txt

でcgroup_enable=cpuset cgroup_enable=memoryを追加。

$sudo apt-get install -y \
     apt-transport-https \
     ca-certificates \
     curl \
     gnupg2 \
     software-properties-common

$ sudo dphys-swapfile swapoff
$ sudo dphys-swapfile uninstall
$ sudo update-rc.d dphys-swapfile remove

Dockerのインストール

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
$ echo "deb [arch=armhf] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
     $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list

$ sudo apt-get update
$ sudo apt-get -y install docker-ce

kubeadmのインストール

現時点でlatestであった1.11をインストールして進めるとkubeadm initの段階でエラーが発生したので、1.9.7を使っている。GKEと同じバージョン。

$ curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
$ echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list

$ sudo apt-get update
$ sudo apt-get install -y kubelet=1.9.7-00 kubeadm=1.9.7-00 kubectl=1.9.7-00

ちなみにkubeadm v1.11でinitしようとすると

Jul 14 20:19:12 k8s-node1 kubelet[5447]: E0714 20:19:12.365501    5447 reflector.go:205] k8s.io/kubernetes/pkg/kubelet/kubelet.go:455: Failed to list v1.Service: Get https://192.168.13.7:6443/api/v1/services?limit=500&resourceVersion=0: dial tcp 192.168.13.7:6443: connect: connection refused

ってエラーが出たから下記のissueに従って変更。

github.com

masterのセットアップ

今回マスターが仕様するIPアドレスは192.168.13.7だったので、このアドレスを指定してノードをクラスタにする必要がある。そのための--apiserver-advertise-addressへの登録。initに成功したら素直に支持されたコマンドでconfigファイルをコピー。

$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=192.168.13.7 --ignore-preflight-errors=cri

Your Kubernetes master has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of machines by running the following on each node
as root:

  kubeadm join --token xxxxxx.xxxxxxxxxxxxx 192.168.13.7:6443 --discovery-token-ca-cert-hash xxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

nodeのセットアップ

ノード端末の方でもinit時に指定されていたコマンドでmasterに参加。

sudo kubeadm join --token xxxxxxxxxxxxxxxxxxxxxxxx 192.168.13.7:6443 --discovery-token-ca-cert-hash xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

とりあえずできた

ちゃんとレジストされているか確認。

$ kubectl get node
NAME         STATUS     ROLES     AGE       VERSION
k8s-master   NotReady   master    2h        v1.9.7
k8s-node1    Ready      <none>    1h        v1.9.7
k8s-node2    NotReady   <none>    1h        v1.9.7

できてた。

Pythonのクロージャ理解する

少し複雑なデコレータを実装したくなったけど、クロージャの挙動ちゃんと理解してないなと思いFluent Pythonで復習しました。
使ったのは第7章5節と6節です。

Fluent Python ―Pythonicな思考とコーディング手法

Fluent Python ―Pythonicな思考とコーディング手法

関数の外部で定義された非グローバルな変数うにどのようにアクセスしているか確認します。
例として挙がっていたのは値が除々に追加されているリストの平均値を計算するようなavg関数です。

まずはクラスを使って実装すると下記のようになります。

class Averager():


  def __init__(self):
    self.series = []

  def __call__(self, new_value):
    self.series.append(new_value)
    total = sum(self.series)
    return total/len(self.series)

if __name__ == "__main__":
  avg = Averager()
  print(avg(10))
  print(avg(11))
  print(avg(12))
実行結果:
10.0
10.5
11.0

このときavgはAveragerのインスタンス。
次は同様のものを関数型で実装してみる。

def make_averager():
  series = []

  def averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

  return averager

if __name__ == "__main__":
  avg = make_averager()
  print(avg(10))
  print(avg(11))
  print(avg(12))
実行結果:
10.0
10.5
11.0

このときのavgは make_averager()の返り値であるaverager関数。
avg関数はseriesというaveragerの外で定義されている変数を参照している.

次に、totalやlen(series)を毎回計算するのは効率が悪いので、これまでの値の総和や要素数のみを保持するように書き換えます。

def make_averager():
  count = 0
  total = 0

  def averager(new_value):
    count += 1
    total += new_value
    return total / count

  return averager

if __name__ == "__main__":
  avg = make_averager()
  print(avg(10))
  print(avg(11))
  print(avg(12))

するとエラーがでる。

実行結果:
Traceback(most recent call last):
 ...
UnboundLocalError: local variable 'count' referenced before assignment

ここでcountとtotalはaveragerの中で再代入されているのでローカル変数扱いとなってしまいます。
参照はOk,再代入はできない。
けど、nonlocal宣言を使うとこのエラーを回避できる。

def make_averager():
  count = 0
  total = 0

  def averager(new_value):
    nonlocal count, total
    count += 1
    total += new_value
    return total / count

  return averager

if __name__ == "__main__":
  avg = make_averager()
  print(avg(10))
  print(avg(11))
  print(avg(12))
実行結果:
10.0
10.5
11.0

クロージャの挙動なんとなく理解できたんで、頑張ってデコレータの実装します。