CLOVER🍀

That was when it all began.

OpenShift Origin(OKD)に、Node.jsアプリケーションをデプロイしていろいろ試す

MinishiftでOpenShift Origin(OKD)を始めると、チュートリアルでNode.jsのアプリケーションをデプロイする例が登場します。

こちらですね。

Minishift Quickstart / Deploying a Sample Application

$ oc new-app https://github.com/openshift/nodejs-ex -l name=myapp

これを、もう少し自分で小さく小さく初めてみたいと思います。

環境

対象の環境は、こちら。

$ minishift status
Minishift:  Running
Profile:    minishift
OpenShift:  Running (openshift v3.9.0+71543b2-33)
DiskUsage:  18% of 19G (Mounted On: /mnt/sda1)
CacheUsage: 1.946 GB (used by oc binary, ISO or cached images)

$ minishift version
minishift v1.22.0+7163416

アプリケーション

アプリケーションは、小さく小さく作ってみます。Expressで、ホスト名を返すだけのアプリケーションを作成してみましょう。

Expressのインストール。

$ npm i --save express

バージョン。

  "dependencies": {
    "express": "^4.16.3"
  }

ソースコード。

src/hostname.js
const express = require('express');
const app = express();

const os = require('os');

app.get('/hostname',
        (req, res) => res.send(`this server name = ${os.hostname()}`));

app.listen(8080);

「npm start」でこのサーバーが起動するように、package.jsonを指定しておきます。

  "scripts": {
    "start": "node src/hostname.js"
  },

アプリケーションの名前は、「node-hostname」としています。

とりあえず、デプロイしてみる

このソースコードをGitリポジトリに登録して、デプロイしてみましょう。次のような感じで、デプロイできます。

$ oc new-app http://....(GitリポジトリのURL)

$ oc new-app openshift/nodejs~http://....(GitリポジトリのURL)

$ oc new-app http://....(GitリポジトリのURL) -i openshift/nodejs

Routeをexposeして

$ oc expose svc/node-hostname
route "node-hostname" exposed

確認。

$ oc get route node-hostname
NAME            HOST/PORT                                       PATH      SERVICES        PORT       TERMINATION   WILDCARD
node-hostname   node-hostname-myproject.192.168.99.100.nip.io             node-hostname   8080-tcp                 None

$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-1-xg6cbk

OKそうです。

Podを増やしてみる

現状、このDeploymentConfigはひとつのみで動いてますが、

$ oc get dc node-hostname
NAME            REVISION   DESIRED   CURRENT   TRIGGERED BY
node-hostname   1          1         1         config,image(node-hostname:latest)


$ oc get pod
NAME                    READY     STATUS      RESTARTS   AGE
node-hostname-1-build   0/1       Completed   0          3m
node-hostname-1-xg6cb   1/1       Running     0          2m

これをスケールアウトさせてみましょう。

Basic Deployment Operations / Manual Scaling

3つにしてみます。

$ oc scale dc node-hostname --replicas=3
deploymentconfig "node-hostname" scaled

3つになりましたね。

$ oc get dc node-hostname
NAME            REVISION   DESIRED   CURRENT   TRIGGERED BY
node-hostname   1          3         3         config,image(node-hostname:latest)


$ oc get pod
NAME                    READY     STATUS      RESTARTS   AGE
node-hostname-1-build   0/1       Completed   0          5m
node-hostname-1-pjn9n   1/1       Running     0          35s
node-hostname-1-ss7pz   1/1       Running     0          35s
node-hostname-1-xg6cb   1/1       Running     0          4m

curlでアクセスした際にも、各Podに順々にアクセスされているようです。

$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-1-ss7pz
$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-1-xg6cb
$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-1-pjn9n
$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-1-ss7pz
$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-1-xg6cb
$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-1-pjn9n

Pod数を減らす時には、「--replicas」を小さい値で指定すればよいです。

$ oc scale dc node-hostname --replicas=1

環境変数を設定してみる

今度は、実行時に使う、環境変数を設定してみましょう。「oc set env」を使うようです。

Managing Environment Variables

アプリケーションを、「ENV_VAR」という名前の環境変数を使うように修正して、デプロイしてみます。

let envVar = process.env.ENV_VAR;

if (envVar === null || envVar === undefined) {
    envVar = 'default env-var';
}

app.get('/hostname',
        (req, res) => res.send(`this server name = ${os.hostname()}, env = [${envVar}]`));

ビルド開始。

$ oc start-build node-hostname

デプロイされました。

$ oc get dc node-hostname
NAME            REVISION   DESIRED   CURRENT   TRIGGERED BY
node-hostname   2          3         3         config,image(node-hostname:latest)

確認。

$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-2-gxr6r, env = [default env-var]

値が入っていません…と。出力されているのは、未設定時のデフォルト値です。

では、環境変数を設定してみます。

$ oc set env dc node-hostname ENV_VAR=myvalue
deploymentconfig "node-hostname" updated

確認。

$ oc set env dc node-hostname --list
# deploymentconfigs node-hostname, container node-hostname
ENV_VAR=myvalue

すると、再度デプロイが行われ、値が設定されます。この時、DeploymentConfigのリビジョンも+1されます。

$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-3-jlz5f, env = [myvalue]

環境変数を削除するには、環境変数名の後ろに「-」を付与します。

$ oc set env dc node-hostname ENV_VAR-
deploymentconfig "node-hostname" updated

環境変数がなくなり

$ oc set env dc node-hostname --list
# deploymentconfigs node-hostname, container node-hostname

アプリケーションも環境変数が設定されていないと認識するようになりました。

$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-4-wrffz, env = [default env-var]

コンテナに引数を設定してみる

環境変数に加えて、今度は起動引数を設定してみましょう。

Basic Deployment Operations / Executing Commands Inside a Container

…これは、YAMLでないと設定できそうにない感じがします。

プログラムを、起動引数を使うように修正。

let envVar = process.env.ENV_VAR;

if (envVar === null || envVar === undefined) {
    envVar = 'default env-var';
}

let argVal;

if (process.argv.length > 2) {
    argVal = process.argv[2];
} else {
    argVal = 'default arg-val';
}

app.get('/hostname',
        (req, res) => res.send(`this server name = ${os.hostname()}, env = [${envVar}], arg = [${argVal}]`));

Gitリポジトリにcommit/pushして、ビルド。

$ oc start-build node-hostname

確認してみますが、当然ながらこの時点では引数はありません。

$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-5-7dk5r, env = [default env-var], arg = [default arg-val]

で、YAMLなら設定できそうな感じがするので、「oc edit」で編集。

$ oc edit dc/node-hostname

この場合、「npm」から定義しなおさないといけないようです。spec / containersの配下の、args要素で指定します。

    spec:
      containers:
      - image: ...
        imagePullPolicy: Always
        name: node-hostname
        ports:
        - containerPort: 8080
          protocol: TCP
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        args:
        - npm
        - start
        - arg1

反映されました。

$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-8-bmwjx, env = [default env-var], arg = [arg1]

ですがまあ、これは不正解な気がします。

Node.jsのS2Iの場合は、環境変数NODE_RUNを使うのが正解な気が。

$ oc set env dc node-hostname NPM_RUN='start arg1'
deploymentconfig "node-hostname" updated

$ curl node-hostname-myproject.192.168.99.100.nip.io/hostname
this server name = node-hostname-6-5z2vd, env = [default env-var], arg = [arg1]

Node.jsのImageStream?

ところで、Node.jsでアプリケーションを作った時に公開されるポートは、どうやって決まってるんでしょうね?

今回は、Node.jsのver 8を使っています。これのS2I builderのDockerfileを見ると
https://github.com/sclorg/s2i-nodejs-container/blob/master/8/Dockerfile

8080ポートがEXPOSEされています。

EXPOSE 8080

これが元ってことでいいんでしょうか?

ビルドはこちら、
https://github.com/sclorg/s2i-nodejs-container/blob/master/8/s2i/bin/assemble

実行はこちらですね。
https://github.com/sclorg/s2i-nodejs-container/blob/master/8/s2i/bin/run

こういうのを見ると、使われている環境変数がわかってよいですね。

実行はs2i/bin/runを見ると

exec npm run -d $NPM_RUN

なのですが、この「NPM_RUN」環境変数が定義されているのは、Dockerfileになります。

ENV NODEJS_VERSION=8 \
    NPM_RUN=start \
    NAME=nodejs \
    NPM_CONFIG_PREFIX=$HOME/.npm-global \
    PATH=$HOME/node_modules/.bin/:$HOME/.npm-global/bin/:$PATH

先の、プログラムの引数を見ていたのは、ここになりますね。

なんとなく、読めるようになってきた気がします。

追記)
この裏取りの方法について、@nekopさんにエントリを書いていただきました。ありがとうございます。

OpenShiftでコンテナの詳細を調べる - nekop's blog

YAMLで書こう

とまあ、ここまでYAMLで書いてきましたが、最後にYAMLにまとめてみたいと思います。

ImageStream。
node-hostname-is.yml

apiVersion: v1
kind: ImageStream
metadata:
  name: node-hostname

作成。

$ oc create -f node-hostname-is.yml
imagestream "node-hostname" created

BuildConfig。
node-hostname-bc.yml

apiVersion: v1
kind: BuildConfig
metadata:
  name: node-hostname
spec:
  triggers:
    - type: ConfigChange
    - imageChange: {}
      type: ImageChange
  source:
    git:
      uri: [GitリポジトリのURL]
      ref: master
    type: Git
  strategy:
    type: Source
    sourceStrategy:
      from:
        kind: ImageStreamTag
        name: nodejs:8
        namespace: openshift
  output:
    to:
      kind: ImageStreamTag
      name: node-hostname:latest

作成。

$ oc create -f node-hostname-bc.yml
buildconfig "node-hostname" created

DeploymentConfig。
node-hostname-dc.yml

apiVersion: v1
kind: DeploymentConfig
metadata:
  name: node-hostname
spec:
  template:
    metadata:
      labels:
        name: node-hostname
        deploymentconfig: node-hostname
    spec:
      containers:
      - name: node-hostname
        image: node-hostname:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
          protocol: TCP
        env:
          - name: ENV_VAR
            value: my value
#          - name: NPM_RUN
#            value: start arg1
        args:
          - npm
          - start
          - arg1
      restartPolicy: Always
  replicas: 3
  selector:
    name: node-hostname
  triggers:
  - type: ConfigChange
  - type: ImageChange
    imageChangeParams:
      automatic: true
      containerNames:
      - node-hostname
      from:
        kind: ImageStreamTag
        name: node-hostname:latest

作成。

$ oc create -f node-hostname-dc.yml
deploymentconfig "node-hostname" created

Service。
node-hostname-svc.yml

apiVersion: v1
kind: Service
metadata:
  name: node-hostname
spec:
  ports:
  - name: 8080-tcp
    port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    deploymentconfig: node-hostname

作成。

$ oc create -f node-hostname-svc.yml
service "node-hostname" created

Route。
node-hostname-route.yaml

apiVersion: v1
kind: Route
metadata:
  name: node-hostname
spec:
  port:
    targetPort: 8080-tcp
  to:
    kind: Service
    name: node-hostname

作成。

$ oc create -f node-hostname-route.yaml
route "node-hostname" created

と、ocコマンドでやってきたことを、だいたい再現できましたと。

オマケ

YAMLを、全部まとめる場合はこんな感じに。

apiVersion: v1
kind: List
items:
  ## ImageStream
  - apiVersion: v1
    kind: ImageStream
    metadata:
      name: node-hostname

  ## BuildConfig
  - apiVersion: v1
    kind: BuildConfig
    metadata:
      name: node-hostname
    spec:
      triggers:
        - type: ConfigChange
        - imageChange: {}
          type: ImageChange
      source:
        git:
          uri: http://192.168.0.2:18080/git/root/node-hostname.git
          ref: master
        type: Git
      strategy:
        type: Source
        sourceStrategy:
          from:
            kind: ImageStreamTag
            name: nodejs:8
            namespace: openshift
      output:
        to:
          kind: ImageStreamTag
          name: node-hostname:latest

  ## DeploymentConfig
  - apiVersion: v1
    kind: DeploymentConfig
    metadata:
      name: node-hostname
    spec:
      template:
        metadata:
          labels:
            name: node-hostname
            deploymentconfig: node-hostname
        spec:
          containers:
          - name: node-hostname
            image: node-hostname:latest
            imagePullPolicy: Always
            ports:
            - containerPort: 8080
              protocol: TCP
            env:
              - name: ENV_VAR
                value: my value
#             - name: NPM_RUN
#               value: start arg1
            args:
              - npm
              - start
              - arg1
        restartPolicy: Always
      replicas: 3
      selector:
        name: node-hostname
      triggers:
        - type: ConfigChange
        - type: ImageChange
          imageChangeParams:
            automatic: true
            containerNames:
              - node-hostname
            from:
              kind: ImageStreamTag
              name: node-hostname:latest

  ## Service
  - apiVersion: v1
    kind: Service
    metadata:
      name: node-hostname
    spec:
      ports:
      - name: 8080-tcp
        port: 8080
        protocol: TCP
        targetPort: 8080
      selector:
        deploymentconfig: node-hostname

  ## Route
  - apiVersion: v1
    kind: Route
    metadata:
      name: node-hostname
    spec:
      port:
        targetPort: 8080-tcp
      to:
        kind: Service
        name: node-hostname