LoginSignup
7
5

More than 5 years have passed since last update.

Kubernetes Job(CronJob)でNode-RED flowを動かす

Last updated at Posted at 2018-09-26

KubernetesでNode-REDを動かす

KubernetesでNode-REDを動かす方法は何種類かあって、Node-REDが動作するコンテナをビルドして Kubernetes で実行するまでを解説のように、コンテナを外部公開するようなエンドポイントを提供する感じが一般的です。

そうじゃなくて、必要な時にコンテナを起動して処理したらコンテナが落ちるような感じにしたい場合は JobCronJob というのを使うっぽいです。

Node-REDをどうやって終了させるか?

Jobは処理を終了させる必要がありますが、ご存知の通りNode-REDはソレ単体では起動し続けるように作られているので、必要な処理が終わったらNode-REDの外からNode-REDのプロセスを終了させるか、Node-REDのFlowでプロセスを終了させなければいけません。

当初はfunctionノードで process.exit してみましたが、functionノードでは process が使えなかったので、今回はnode-red-contrib-exitという任意のExit codeを指定してNode-REDを終了させるだけの単純なノードを作りました。

実際にJobとして作成したFlow

Flow作成する前にCredentialの関係でエディタが参照しているsettings.jsを以下のように編集する必要があります。

// By default, credentials are encrypted in storage using a generated key. To
// specify your own secret, set the following property.
// If you want to disable encryption of credentials, set this property to false.
// Note: once you set this property, do not change it - doing so will prevent
// node-red from being able to decrypt your existing credentials and they will be
// lost.
credentialSecret: "<適当な文字列>", 

で、実際 node-red-contrib-exit を使ってメールが正常に送信されたら exit 0 を、例外が発生したら exit 1 で成否に関係なくNode-REDを終了させるFlowが以下です。

image.png

[{
    "id": "a88f2597.ecc188",
    "type": "subflow",
    "name": "isProd",
    "info": "",
    "in": [{
        "x": 54,
        "y": 69,
        "wires": [{
            "id": "334b358a.c7ab6a"
        }]
    }],
    "out": [{
        "x": 341,
        "y": 125,
        "wires": [{
            "id": "7640fbaf.846f84",
            "port": 0
        }]
    }, {
        "x": 341,
        "y": 177,
        "wires": [{
            "id": "7640fbaf.846f84",
            "port": 1
        }]
    }]
}, {
    "id": "334b358a.c7ab6a",
    "type": "change",
    "z": "a88f2597.ecc188",
    "name": "",
    "rules": [{
        "t": "set",
        "p": "env.node_env",
        "pt": "msg",
        "to": "NODE_ENV",
        "tot": "str"
    }],
    "action": "",
    "property": "",
    "from": "",
    "to": "",
    "reg": false,
    "x": 206,
    "y": 69,
    "wires": [
        ["ce0f4b8d.e9cc38"]
    ]
}, {
    "id": "ce0f4b8d.e9cc38",
    "type": "process-env",
    "z": "a88f2597.ecc188",
    "name": "",
    "into": "payload",
    "x": 426.5,
    "y": 69,
    "wires": [
        ["7640fbaf.846f84"]
    ]
}, {
    "id": "7640fbaf.846f84",
    "type": "switch",
    "z": "a88f2597.ecc188",
    "name": "",
    "property": "payload.node_env",
    "propertyType": "msg",
    "rules": [{
        "t": "eq",
        "v": "production",
        "vt": "str"
    }, {
        "t": "else"
    }],
    "checkall": "true",
    "repair": false,
    "outputs": 2,
    "x": 222,
    "y": 132,
    "wires": [
        [],
        []
    ]
}, {
    "id": "abb7cf3b.9f9c3",
    "type": "inject",
    "z": "e618397e.9b1cd8",
    "name": "",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "repeat": "",
    "crontab": "",
    "once": true,
    "onceDelay": "",
    "x": 113.5,
    "y": 56,
    "wires": [
        ["4b7c7186.23f76"]
    ]
}, {
    "id": "984fd139.d0db1",
    "type": "exit",
    "z": "e618397e.9b1cd8",
    "name": "",
    "exitcode": "0",
    "x": 526.5,
    "y": 174,
    "wires": []
}, {
    "id": "fa2df919.fd9058",
    "type": "debug",
    "z": "e618397e.9b1cd8",
    "name": "",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "x": 204.5,
    "y": 128,
    "wires": []
}, {
    "id": "4b7c7186.23f76",
    "type": "subflow:a88f2597.ecc188",
    "z": "e618397e.9b1cd8",
    "x": 255.5,
    "y": 57,
    "wires": [
        ["fddf94ba.c3fb78"],
        ["fa2df919.fd9058"]
    ]
}, {
    "id": "bad0f0c1.9fba4",
    "type": "e-mail",
    "z": "e618397e.9b1cd8",
    "server": "smtp.gmail.com",
    "port": "465",
    "secure": true,
    "name": "to@example.com",
    "dname": "send mail",
    "x": 450.5,
    "y": 128,
    "wires": []
}, {
    "id": "fddf94ba.c3fb78",
    "type": "change",
    "z": "e618397e.9b1cd8",
    "name": "",
    "rules": [{
        "t": "set",
        "p": "from",
        "pt": "msg",
        "to": "from@example.com",
        "tot": "str"
    }, {
        "t": "set",
        "p": "payload",
        "pt": "msg",
        "to": "This email was sent by kubernetes job.",
        "tot": "str"
    }, {
        "t": "set",
        "p": "topic",
        "pt": "msg",
        "to": "Mail from kubernetes job.",
        "tot": "str"
    }],
    "action": "",
    "property": "",
    "from": "",
    "to": "",
    "reg": false,
    "x": 428.5,
    "y": 52,
    "wires": [
        ["bad0f0c1.9fba4"]
    ]
}, {
    "id": "25c8c61e.4eaeea",
    "type": "status",
    "z": "e618397e.9b1cd8",
    "name": "",
    "scope": ["bad0f0c1.9fba4"],
    "x": 100.5,
    "y": 188,
    "wires": [
        ["f5ea97f9.11a008"]
    ]
}, {
    "id": "f5ea97f9.11a008",
    "type": "switch",
    "z": "e618397e.9b1cd8",
    "name": "",
    "property": "status.text",
    "propertyType": "msg",
    "rules": [{
        "t": "eq",
        "v": "",
        "vt": "str"
    }, {
        "t": "else"
    }],
    "checkall": "true",
    "repair": false,
    "outputs": 2,
    "x": 258.5,
    "y": 188,
    "wires": [
        ["26742b2c.b82bd4"],
        []
    ]
}, {
    "id": "b7318bfe.a1d6f8",
    "type": "catch",
    "z": "e618397e.9b1cd8",
    "name": "",
    "scope": null,
    "x": 95.5,
    "y": 244,
    "wires": [
        ["91d21df.ea048e"]
    ]
}, {
    "id": "73257e5a.6466",
    "type": "exit",
    "z": "e618397e.9b1cd8",
    "name": "",
    "exitcode": "1",
    "x": 396,
    "y": 242,
    "wires": []
}, {
    "id": "26742b2c.b82bd4",
    "type": "subflow:a88f2597.ecc188",
    "z": "e618397e.9b1cd8",
    "x": 396,
    "y": 181,
    "wires": [
        ["984fd139.d0db1"],
        ["dc9c3f20.7b90f"]
    ]
}, {
    "id": "dc9c3f20.7b90f",
    "type": "debug",
    "z": "e618397e.9b1cd8",
    "name": "",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "x": 547,
    "y": 227,
    "wires": []
}, {
    "id": "91d21df.ea048e",
    "type": "subflow:a88f2597.ecc188",
    "z": "e618397e.9b1cd8",
    "x": 250,
    "y": 244,
    "wires": [
        ["73257e5a.6466"],
        ["b45f94ce.59c9e8"]
    ]
}, {
    "id": "b45f94ce.59c9e8",
    "type": "debug",
    "z": "e618397e.9b1cd8",
    "name": "",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "x": 417,
    "y": 297,
    "wires": []
}]

以下、Flowの解説になります。

Node-REDが起動したら1回だけ動くinjectノード

Node-REDをどう終了させるかも大事ですが、Node-RED起動時に自動的にFlowをスタートさせるのも地味に大事です。

今回はinjectノードの「Node-RED起動のx秒後、以下を行う」(英語だと「Inject once at start?」)をチェックすることで実現します。

image.png

環境変数で動作を振り分けるSub Flow

Sub Flowがありますが、これはnode-red-contrib-process-envを使って、 NODE_ENV の値が production の場合にだけ、Inject once at startやexitノードによるNode-RED終了が行われるようにしています。

image.png

Flow開発中はSub Flowの後続に別のinjectノードを置いて開発します。

Kubernetes CronJobの設定

今回はDocker for MacのローカルKubernetes環境を使っています(参考:Docker for MacでKubernetes インストールからデプロイまで

Dockerイメージの作成

上記Flowを内蔵したNode-REDが動くコンテナイメージを作成します。

まずは、本家のnode-red-dockerをクローンします。

$ git clone https://github.com/node-red/node-red-docker

Credentialの関係で直下(package.jsonと同じ階層)に先ほど作成したFlowの flows.json, cred.json, settings.js を作成します。Flowのエクスポートで取得できるJSONとは異なりますのでFlowを作成したエディタが参照している各ファイルをコピーします

こんな感じ。

  .
  ├── CONTRIBUTING.md
  ├── LICENSE
  ├── README.md
  ├── aarch64
  │   └── Dockerfile
  ├── build-rpi.sh
+ ├── cred.json
+ ├── flows.json
  ├── latest
  │   └── Dockerfile
  ├── package.json
  ├── rpi
  │   └── Dockerfile
+ ├── settings.js
  ├── slim
  │   └── Dockerfile
  ├── update.sh
  └── version.sh

Node-REDのCredentialファイルには機密情報(今回はe-mailノードのsmtp接続情報とか)が暗号化されて記載されています。これを復号するために必要なキーが冒頭settings.jsに指定した credentialSecret です。

今回はJobという性質上、コンテナを作成してからFlowを実行して終了するまで全て自動化しなければいけませんのでエディタで機密情報を入力する暇がありません。このように全てのファイルをコピーします。

続いて、package.jsonを編集します。

追加インストールするノード。

 "dependencies": {
-    "node-red": "0.19.4",
-    "node-red-node-msgpack": "*",
-    "node-red-node-base64": "*",
-    "node-red-node-suncalc": "*",
-    "node-red-node-random": "*"
-},
-"engines": {
-    "node": "4.*.*"
+    "node-red": "*",
+    "node-red-contrib-exit": "*",
+    "node-red-contrib-process-env": "*"
 }

Credentialの関係でflow.jsonは環境変数で指定しないように。


  "scripts": {
-    "start": "node $NODE_OPTIONS node_modules/node-red/red.js -v $FLOWS"
+    "start": "node $NODE_OPTIONS node_modules/node-red/red.js"
  },

latest/Dockerfileを編集します。

ここもCredentialの関係でFlowを作成したエディタが参照していた各ファイルをビルド時にイメージへコピーするよう設定します。また、ホスト名から生成される各ファイル名を決め打ちしています。

 # package.json contains Node-RED NPM module and node dependencies
+COPY flows.json /data/flows_sendmail-nodered-job.json
+COPY cred.json /data/flows_sendmail-nodered-job_cred.json
+COPY settings.js /data/settings.js
 COPY package.json /usr/src/node-red/
 RUN npm install

したがって、環境変数 FLOWS は削除して HOSTNAME でホスト名を指定します(ただし、実際にコンテナのホスト名を指定するには後ほど作成するManifestファイルでもホスト名を指定する必要があります)

 # Environment variable holding file path for flows configuration
-ENV FLOWS=flows.json
+ENV HOSTNAME=sendmail-nodered-job
 ENV NODE_PATH=/usr/src/node-red/node_modules:/data/node_modules

docker buildします。

$ docker build -f ./latest/Dockerfile -t sendmail-nodered-job .

docker imagesで確認。

$ docker images

REPOSITORY            TAG     IMAGE ID      CREATED              SIZE
sendmail-nodered-job  latest  9de0ab5ec677  About a minute ago   718MB
...

CronJobのManifestファイルを作成

CronJobのManifestファイルを作成します。

sendmail-nodered-job.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: sendmail-nodered-job
  labels:
    cronjob: sendmail-nodered-job
spec:
  concurrencyPolicy: Replace
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          hostname: sendmail-nodered-job
          containers:
          - name: sendmail-nodered-job
            image: sendmail-nodered-job
            imagePullPolicy: IfNotPresent
            env:
              - name: NODE_ENV
                value: production
          restartPolicy: OnFailure

以下、解説。

並列実行のポリシーを concurrencyPolicy で指定します(参考:KubernetesのCron Jobを使って雑務を自動化する

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

1分間隔でメール送信するNode-RED Flowを起動します。

schedule: "*/1 * * * *"

ホスト名を指定。

hostname: sendmail-nodered-job

先ほど作成したDockerイメージを指定。

image: sendmail-nodered-job

pullするDockerイメージがローカルの場合は以下の設定が必要。

imagePullPolicy: IfNotPresent

先ほどFlowの処理を環境変数によって振り分けたのでココで NODE_ENVproduction を設定。

env:
    - name: NODE_ENV
      value: production

Podやコンテナの障害処理ポリシーを指定(参考:Kubernetes "ジョブ" についての自習ノート

restartPolicy: OnFailure

CronJob実行

CronJobを作成します。

$ kubectl create -f sendmail-nodered-job.yaml 
cronjob.batch "sendmail-nodered-job" created

CronJobの確認。

$ kubectl get cronjob
NAME                   SCHEDULE      SUSPEND   ACTIVE    LAST SCHEDULE   AGE
sendmail-nodered-job   */1 * * * *   False     0         <none>          37s

LAST SCHEDULE が更新されたらJobが動いたということ。

$ kubectl get cronjob
NAME                   SCHEDULE      SUSPEND   ACTIVE    LAST SCHEDULE   AGE
sendmail-nodered-job   */1 * * * *   False     0         22s             1m

Job一覧。

$ kubectl get job -a
NAME                              DESIRED   SUCCESSFUL   AGE
sendmail-nodered-job-1538130960   1         0            5s

Podも確認。

$ kubectl get pod
NAME                                    READY     STATUS      RESTARTS   AGE
sendmail-nodered-job-1538130960-9zjwp   0/1       Completed   0          18s

ログを見てみるとNode-REDの起動から終了まで5秒くらい( httpAdminRootfalse にした場合)

$ kubectl logs sendmail-nodered-job-1538130960-9zjwp

> node-red-docker@1.0.0 start /usr/src/node-red
> node $NODE_OPTIONS node_modules/node-red/red.js "--userDir" "/data"

26 Sep 10:36:07 - [info]

Welcome to Node-RED
===================

26 Sep 10:36:07 - [info] Node-RED version: v0.19.4
26 Sep 10:36:07 - [info] Node.js  version: v6.14.4
26 Sep 10:36:07 - [info] Linux 4.9.93-linuxkit-aufs x64 LE
26 Sep 10:36:07 - [info] Loading palette nodes
26 Sep 10:36:08 - [warn] rpi-gpio : Raspberry Pi specific node set inactive
26 Sep 10:36:08 - [warn] rpi-gpio : Cannot find Pi RPi.GPIO python library
26 Sep 10:36:10 - [info] Settings file  : /data/settings.js
26 Sep 10:36:10 - [info] Context store  : 'default' [module=memory]
26 Sep 10:36:10 - [info] User directory : /data
26 Sep 10:36:10 - [warn] Projects disabled : set editorTheme.projects.enabled=true to enable
26 Sep 10:36:10 - [info] Flows file     : /data/flows_sendmail-nodered-job.json
26 Sep 10:36:10 - [info] Admin UI disabled
26 Sep 10:36:10 - [info] Server now running at http://127.0.0.1:1880
26 Sep 10:36:10 - [info] Starting flows
26 Sep 10:36:10 - [info] Started flows
26 Sep 10:36:12 - [info] [e-mail:to@example.com] Message sent: 250 Ok ...

ちなみに、kubectlのCronJobやJobの操作はkubernetesのCronJobを触る がわかりやすいです。

そしてメールが来た。

image.png

CrobJobの削除

$ kubectl delete -f sendmail-nodered-job.yaml
cronjob.batch "sendmail-nodered-job" deleted

本当は例外時をもっと検証しないといけないのですが、とりあえず正常系動いたので公開します。

多分、catchやstatusで制御できないとか処理をブロックするようなノードが入り込んじゃうといつまでも終わらないJobが出来上がる気がする...

おまけ

こういうJob系のDockerイメージはプライベートなDocker Registryに置きたいところですが、そういう場合はgitlab.comGitLab Container Registry使うと良いですよ!なんと無料

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5