GKEでkubernetesベースのシンプルなバッチを動かす

はじめに
===========

Kubernetes(以下、k8s)と言えばマイクロサービスとかのWebアプリとかをコンテナ化する話しの方が一般的な気はしますが、個人的にはDバッチを移行する方にも興味があります。
というのも、仕事柄同時に大量のバッチを流す事が多く、そういう処理はバッチ自体がマルチスレッドのものもあれば、そうじゃないものも混在していてリソース管理がややこしので、そのあたりのリソース管理が出来ると良いなぁ、と。

というわけで、k8sのマネージド環境であるGoogle Kubernetes Engineを使って、バッチを組んでみました。k8sを利用するのは初めてなのでおかしな所があればご指摘ください。

準備
======

バッチ向けのDokcer
------

Dockerfileの時点では特に変わったことはありません。

```bash
FROM google/cloud-sdk

RUN echo '\n\
curl "https://api.coindesk.com/v1/bpi/historical/close.json?start=2018-01-01&end=$(date +%Y-%m-%d)" > btc_history.json \n\
gsutil cp btc_history.json gs://cn_orz_pascal-bitcoin_prediction/ \n\
gsutil ls -l gs://cn_orz_pascal-bitcoin_prediction/ \n\
' > run.sh

CMD sh -x run.sh%   
```

こんな感じでシンプルにCMDで実行します。もちろん、pythonとかgoで書いたコードもCOPYしてCMDで実行してすれば良いだけですね。

```bash
FROM jupyter/scipy-notebook

RUN pip install --upgrade pip && pip install google-cloud

COPY predictor.py predictor.py
CMD python predictor.py
```


Build & Push
-----------

ビルドも普通に実質すれば良いのですが、Google Container Registryを使う時は`gcr.io/{project name}/{container name}` というフォーマットにしてやる必要があります。

```bash
docker build -t gcr.io/koduki-docker-test-001-1083/collector .
gcloud docker -- push gcr.io/koduki-docker-test-001-1083/collector
```

鍵などの秘密情報
------------

Cloud Storageなどのサービスを利用するためには鍵情報が必要です。コンテナ内に含むわけには当然行かないので以下のように実施します。

```bash
kubectl create secret generic btc-prediction-key --from-file=key.json=~/koduki-docker-test-001-1083-xxxxxxx.json
```

これで`btc-prediction-key`というsecretに`key.json`という名前で`koduki-docker-test-001-1083-xxxxxxx.json`が配置されたのでデプロイ時に指定することで利用できます。

デプロイ時に指定する項目を抜粋すると以下の通り

```yml
spec:
    volumes:
    - 
        name: google-cloud-key
        secret:
            secretName: btc-prediction-key
.
.
.
    volumeMounts:
    - 
        name: google-cloud-key
        mountPath: /var/secrets/google
    env:
    - 
        name: GOOGLE_APPLICATION_CREDENTIALS
        value: /var/secrets/google/key.json
```

Cloud StorageなどのGCPのAPIは環境変数の`GOOGLE_APPLICATION_CREDENTIALS`を確認して鍵ファイルをみる仕様になっているので、mountしたsecretの値を格納します。

また、k8sのクラスタを作るときにデフォルトは他のGCPサービスを利用できない設定になっているので、クラスタ構築時に忘れずに動くようにしておく必要があります。こちらのStack Overflowが参考になります。

- [AccessDeniedException: 403 Insufficient OAuth2 scope to perform this operation. ](https://stackoverflow.com/questions/46497002/gcs-write-access-from-inside-a-gke-pod?noredirect=1&lq=1)


k8sでのバッチの実行
==================

手動で単発のバッチとして実行する
----------

まずは手動でk8s上で実行するには以下のように`kubectl run`を使います。

```bash
kubectl run "btcollector" --restart=OnFailure --image="gcr.io/koduki-docker-test-001-1083/collector"
```

特に標準出力に情報が返って来なければ正解です。

`get job`でジョブの状態を確認してみます。試したのは短いジョブなので既に終わってますが、SUCCESSFULになってる事が確認できます。

```bash
% kubectl get job
NAME                         DESIRED   SUCCESSFUL   AGE
btcollector                  1         1            1m
```

もう少しジョブの詳細が見たいときは`kubectl describe`を使います。どのPodで実行されたのか、などより詳細な内容が分かります。

```bash
% kubectl describe jobs/btcollector
Name:           btcollector
Namespace:      default
Selector:       controller-uid=74f18356-ac64-11e8-a825-42010a800172
Labels:         run=btcollector
.
.
.
```

ログを確認するには`kubectl log`を利用します。こちらで実行したコンテナの標準出力が取得出来ます。

```bash
% kubectl log jobs/btcollector  
W0830 08:02:36.908945   33937 cmd.go:353] log is DEPRECATED and will be removed in a future version. Use logs instead.
+ date +%Y-%m-%d
+ curl https://api.coindesk.com/v1/bpi/historical/close.json?start=2018-01-01&end=2018-08-30
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  5698    0  5698    0     0  10602      0 --:--:-- --:--:-- --:--:-- 10610
+ gsutil cp btc_history.json gs://cn_orz_pascal-bitcoin_prediction/
Copying file://btc_history.json [Content-Type=application/json]...
/ [1 files][  5.6 KiB/  5.6 KiB]                                                
Operation completed over 1 objects/5.6 KiB.                                      
+ gsutil ls -l gs://cn_orz_pascal-bitcoin_prediction/
      5698  2018-08-30T14:53:40Z  gs://cn_orz_pascal-bitcoin_prediction/btc_history.json
     10060  2018-08-29T23:58:36Z  gs://cn_orz_pascal-bitcoin_prediction/btc_prediction.json
TOTAL: 2 objects, 15758 bytes (15.39 KiB)
```

ちなみに実行時の名前は一意にする必要があるようなので２回同じ名前で実行するとエラーになります。Jenkinsなどで外部のジョブスケジューラから叩くときはタイムスタンプ入れるとか注意が必要そうです。

```bash
% kubectl run "btcollector" --restart=OnFailure --image="gcr.io/koduki-docker-test-001-1083/collector"
Error from server (AlreadyExists): jobs.batch "btcollector" already exists
```

スケジューリング実行
---------

先程は手で実行する例を出しましたが、やはり特定の時間になったらスケジューリング実行されるcronのような機能は必要ですよね？
k8sにはその機能はちゃんと用意されています。

今回は記述も長くなるのでrunではなくyamlにジョブの情報を記載してdeployをして見ます。

まずは、ジョブの情報を記載したyamlは下記のようになります。

```yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
    name: btcpredictor
spec:
    concurrencyPolicy: Replace
    schedule: "55 23 * * * "
    jobTemplate:
        spec:
            template:
                spec:
                    volumes:
                    - 
                        name: google-cloud-key
                        secret:
                            secretName: btc-prediction-key
                    containers:
                        -
                            name: btc-predictor-container-01
                            image: gcr.io/koduki-docker-test-001-1083/predictor
                            volumeMounts:
                            - 
                                name: google-cloud-key
                                mountPath: /var/secrets/google
                            env:
                            - 
                                name: GOOGLE_APPLICATION_CREDENTIALS
                                value: /var/secrets/google/key.json
                    restartPolicy: Never
            backoffLimit: 4
```

yamlを使ったdeployは`kubectl create`を利用します。

```bash
% kubectl create -f my-job.yaml
```

これで`gcr.io/koduki-docker-test-001-1083/predictor`が登録されました。毎日23:55に実行される事になります。
cronに登録してもjobである事には変わらないので`kubectl jobs`など手動実行の際と同様のコマンドで管理できます。


まとめ
========

基本的なジョブ実行をk8sで試して見ました。今回はしてませんがコンテナ毎でのCPUやメモリの設定をすればよりアプリ側は環境のリソースをMAX使うようにしておいても上手く運用時に制御が出来るようになるので、なかなか良さそうです。

また、本来であれば単なる時間実行だけでは無く待合せとか後続ジョブとか複雑なジョブ管理が必要になってきます。
[Argo](https://applatix.com/open-source/argo/)とかのWorkflowエンジンを使えば出来るようなので、今度こちらも試して見たいと考えています。

現時点で気になる点としては、まだ私がツールに慣れてないだけかもしれないですが、SparkのCloudera Managerなどのようにジョブ単位でどのくらい今リソースを使ってるとかは分かりづらい感じがしました。
GKEのモニタリング画面だけだと不足しそうなのでこの辺は何らかの追加のDashBoardの作成が必要になりそうです。

それではHappy Hacking!