Node-red on GKE (k8s)

Hi.

I note that there are no official docs for deploying node-red on k8s, so here's what I've done:

---
apiVersion: v1
kind: Service
metadata:
  name: node-red-service
  annotations:
    "external-dns.alpha.kubernetes.io/ttl": "5"
    "external-dns.alpha.kubernetes.io/hostname": <FQDN-HERE>
spec:
  type: LoadBalancer
  selector:
    app: node-red
  ports:
  - protocol: TCP
    port: 443
    targetPort: 1880
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: node-red-cert
spec:
  secretName: node-red-tls
  duration: 2160h # 90d
  renewBefore: 360h # 15d
  subject:
    organizations:
      - <ORG-HERE>
  isCA: false
  privateKey:
    algorithm: RSA
    encoding: PKCS1
    size: 2048
  usages:
    - server auth
    - client auth
  dnsNames:
    - <FQDN-HERE>
  issuerRef:
    name: cluster-letsencrypt-issuer
    kind: ClusterIssuer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  creationTimestamp: null
  labels:
    app: node-red
  name: node-red-data
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: node-red
  name: node-red
  namespace: node-red
spec:
  replicas: 1
  selector:
    matchLabels:
      app: node-red
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: node-red
    spec:
      securityContext:
        fsGroup: 1000
        runAsUser: 1000
        runAsGroup: 1000
      containers:
      - image: nodered/node-red
        command: ['/mnt/config/startup.sh']
        imagePullPolicy: Always
        name: node-red
        ports:
        - containerPort: 1880
          protocol: TCP
        resources: {}
        envFrom:
        - secretRef:
            name: node-red-okta-creds
        - secretRef:
            name: node-red-keys
        env:
        - name: FLOWS
          value: "-s /mnt/config/settings.js"
        - name: WEB_DOMAIN
          value: <FQDN-HERE>
        - name: DO_NPM_INSTALL
          value: passport-okta-oauth
        - name: TZ
          value: UTC
        - name: PGID
          value: "1000"
        - name: PUID
          value: "1000"
        volumeMounts:
          - name: node-red-data
            mountPath: /data
          - name: node-red-config
            mountPath: /mnt/config
          - name: node-red-tls
            mountPath: /mnt/tls
            readOnly: true
      volumes:
      - name: node-red-data
        persistentVolumeClaim:
          claimName: node-red-data
      - name: node-red-config
        configMap:
          name: node-red-configmap
          defaultMode: 0754
      - name: node-red-tls
        secret:
          secretName: node-red-tls
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: node-red-configmap
data:
  settings.js: |
    module.exports = {

      credentialSecret: process.env.CREDS_KEY || false,
      flowFile: 'flows.json',
      flowFilePretty: true,

      adminAuth: {
        type:"strategy",
        strategy: {
          name: "okta",
          label: 'Sign in with Okta',
          icon:"fa-o",
          strategy: require("passport-okta-oauth").Strategy,
          options: {
            audience: process.env.OKTA_AUDIENCE,
            clientID: process.env.OKTA_CLIENTID,
            clientSecret: process.env.OKTA_CLIENTSECRET,
            // idp: process.env.OKTA_IDP,
            scope: ['openid', 'email', 'profile'],
            response_type: 'code',
            callbackURL: "https://" + process.env.WEB_DOMAIN + "/auth/strategy/callback",
            verify: (token, tokenSecret, profile, done) => {
              //console.log(profile)
              return done(null, profile);
            }
          },
        },
        users: (username) =>{
          return Promise.resolve({ username: username, permissions: ["*"] })
        }
      },

      https: {
        key: require("fs").readFileSync('/mnt/tls/tls.key'),
        cert: require("fs").readFileSync('/mnt/tls/tls.crt')
      },
      requireHttps: true,

      uiPort: process.env.PORT || 1880,

      diagnostics: {
        enabled: true,
        ui: true,
      },

      runtimeState: {
          enabled: false,
          ui: false,
      },

      logging: {
          console: {
              level: "info",
              metrics: false,
              audit: true
          }
      },

      exportGlobalContextKeys: false,

      externalModules: {

      },

      editorTheme: {
        palette: {

        },

        projects: {
          enabled: false,
          workflow: {
              mode: "manual"
          }
        },

        codeEditor: {
          lib: "monaco",
          options: {
          }
        }
      },

      functionExternalModules: true,

      functionGlobalContext: {
      },

      debugMaxLength: 1000,
      mqttReconnectTime: 15000,
      serialReconnectTime: 15000,

    }

  startup.sh: |
    #!/bin/sh
    set -e

    cd ~

    for DNI in $DO_NPM_INSTALL; do
      echo "Installing NPM package: $DNI"
      npm install "$DNI"
    done

    exec ./entrypoint.sh $@

Some notes on this setup:

  • Makes use of dns-external to update DNS records
  • Makes use of cert-manager for certificate generation/handling
  • Overrides entrypoint to allow extra packages to be installed (e.g. for auth)
  • Uses Okta OIDC authentication (you dont have to, but I left everything in)
  • Misuses the FLOWS envvar to pass arguments to node-red (e.g. the location of the settings.js from the config map)
  • Although a Deployment, its not safe for replica >1 - As such it should probably be a Pod.