KubernetesのCron Jobを使って雑務を自動化する

# とりあえず

サンプルはここにコミットしてあります。
https://github.com/TakiTake/example-kubernetes-jobs

# 背景

あまりChatbotに機能を盛り過ぎると、運用が辛くなりそう。
crontabは単一のサーバでしか使えず、運用コストが高い。
複雑な依存関係は無いので、[jobscheduler](http://www.sos-berlin.com/jobscheduler)はリッチ過ぎる。
社内の雑務なんでJenkinsでも良いんですが、せっかくKubernetesもあるので勉強がてらCronJobで実装してみたい。

ちなみに、JenkinsならPipeline Pluginの[triggers](https://jenkins.io/doc/book/pipeline/syntax/#triggers)が使えそう。

# Cron Jobとは？
Kubernetes版crontabと思っていただければ、良いかと。
定期的に実行したいタスクをKubernetesに面倒見てもらおう、と言うものです。

今回は、Kubernetes Server側がver1.8だったのでこちらのドキュメントを参考に実装しました。
https://v1-8.docs.kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/

```
$ minikube version
minikube version: v0.24.1

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"9", GitVersion:"v1.9.1", GitCommit:"3a1c9449a956b6026f075fa3134ff92f7d55f812", GitTreeState:"clean", BuildDate:"2018-01-04T19:58:48Z", GoVersion:"go1.9.2", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"8", GitVersion:"v1.8.0", GitCommit:"0b9efaeb34a2fc51ff8e4d34ad9bc6375459c4a4", GitTreeState:"clean", BuildDate:"2017-11-29T22:43:34Z", GoVersion:"go1.9.1", Compiler:"gc", Platform:"linux/amd64"}
```

# 何を自動化したいか？

- Openのまま放置されているチケットの一覧を通知
- MergeされていないPull Requestの一覧を通知
- Weekly Reportページの作成

この辺りが自動化したいタスクかなと。
今回は、最初の例を実装しました。

# Openのまま放置されているチケットの一覧を通知

## ツール群

go-jiraでチケット一覧を取得してSlackboard経由でSlackにコメントを投稿しています。

```
jira list | slackboard-cli ==> slackboard ==> Slack
```

チケットはJIRAで管理されているので、JIRA用のクエリJQLが使えるツールということで
[go-jira](https://github.com/Netflix-Skunkworks/go-jira)を採用。カスタムコマンドが作れるという点もポイント高いです。

通知先は、Slackで[Incoming Webhook](https://api.slack.com/incoming-webhooks)を利用。CURLは原始的過ぎるので、[Slackboard](https://qiita.com/cubicdaiya/items/1f9a8846f55b9d0bcb48)を採用。

## ディレクトリ構成

https://github.com/TakiTake/example-kubernetes-jobs

```
.
├── LICENSE
├── README.md
├── docker
│   ├── jira
│   │   └── Dockerfile
│   ├── slackboard
│   │   └── Dockerfile
│   └── slackboard-cli
│       └── Dockerfile
└── k8s
    ├── apps
    │   └── slackboard
    │       ├── conf
    │       │   └── slackboard.toml
    │       └── slackboard.yaml
    └── cronjobs
        ├── hi-takky
        │   └── hi-takky.yaml
        └── list-unresolved
            ├── conf
            │   ├── entrypoint.sh
            │   └── jira.d
            │       └── config.yml
            └── list-unresolved.yaml
```

dockerディレクトリ以下ではDockerfileを管理しています。
k8sディレクトリ以下のappsでは常駐アプリのmanifestファイルと設定ファイルを、cronjobsでは定期的なタスクのmanifestファイルと設定ファイルを管理しています。

内容に関するポリシーは

- Dockerfileは実行環境の構築に専念して、なるべく汎用的に作る。
- Kubernetes Manifestファイルは、Kubernetesの設定に専念してアプリやジョブのロジックへの依存を少なくする。
- 変数や設定は、環境変数やConfigMapとして外出しする。

## CronJob

```yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: list-unresolved
  labels:
    cronjob: list-unresolved
spec:
  concurrencyPolicy: Replace
  schedule: "50 8 * * 1-5"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: list-unresolved
            image: jira
            imagePullPolicy: IfNotPresent
            env:
              - name: JIRA_USERNAME
                value: takeshi.takizawa
              - name: JIRA_PASSWORD
                value: ***
            args:
            - "/bin/sh"
            - "/root/entrypoint.sh"
            volumeMounts:
              - name: entrypoint
                mountPath: /root/entrypoint.sh
                subPath: entrypoint.sh
              - name: jirad
                mountPath: /root/.jira.d
          restartPolicy: OnFailure
          volumes:
            - name: entrypoint
              configMap:
                name: list-unresolved-entrypoint
            - name: jirad
              configMap:
                name: list-unresolved-jirad
```

Deploymentと結構似ているので、そんなに迷わずに書けました。

Concurrent Policyは以下の3つから選べます。

- Allow (default): 同時実行を許可する。
- Forbid: 一つ前のジョブが動いていた場合は、スキップする。
- Replace: 現在動いているジョブをキャンセルして、新しいジョブと入れ替える。

Scheduleは、crontabと同じフォーマットで書けます。

その他の項目は、Deploymentと同じなので割愛。
JIRAのパスワードがベタ書きなのは、おいおい直します。。

## entrypoint.sh

```sh
echo ${JIRA_PASSWORD} | /root/jira login --user=${JIRA_USERNAME}
/root/jira list-unresolved | /root/slackboard-cli -t general -s ${SLACKBOARD_SERVICE_HOST}:${SLACKBOARD_SERVICE_PORT}
```

シェルの中身は、こんなものです。go-jiraのカスタムコマンドを定義しているのでかなりシンプルになりました。

## .jira.d/config.yml

```yaml
queries:
  unresolved: |
    project = "{{.project}}" AND labels = "External" AND resolution = Unresolved ORDER BY createdDate ASC

custom-commands:
  - name: list-unresolved
    help: display unresolved issues come from external
    script: |-
      {{jira}} list --named-query "unresolved" --project "DEVOPS"
```

カスタムコマンドの定義は、こんな感じです。JQLはJQLで別途定義できるので、横に長くなり過ぎないのが気に入っています。

# まとめ

DockerfileやManifestファイルに起こすのは手間でしたが、docker buildしてkubectl createしたら、どこでも動くという手軽さは素晴らしいなと。
そして何より、何が必要か全部書いているので暗黙の前提ってやつを避けられるのは、新人の教育や、引き継ぎ時に大変役立ちます。