NodeRED I: Instalación y ejemplo base

En la presente entrada, NodeRED I: Instalación, realizaré una descripción de la herramienta NodeRed y explicaré los primeros pasos para utilizar NodeRED mediante la definición de un programa básico representado en un flujo NodeRED.

Node-RED es aquella herramienta de programación que permite la conexión de dispositivos hardware, API y servicios online.

El editor de Node-RED está basado en una herramienta gráfica que se accede desde el navegador web La estructura de la interfaz gráfica está compuesta en la parte central por un panel de trabajo; en la parte izquierda, se muestra los diferentes nodos funcionales categorizados por funcionalidades; y, en la parte derecha, un conjunto de pantallas en donde se puede visualizar diferentes paneles de información.

Un programa de NodeRED es aquella estructura visual de un flujo de operaciones compuesta por la unión de nodos la cual realiza una funcionalidad determinada a partir de los datos de entrada definidos en los nodos para tal fin y, como salida, un resultado determinado en los nodos de salida.

Instalación en Docker

NodeRED puede ser instalado en varias plataformas: en una máquina local, en un contenedor docker, en un dispositivo como puede ser una placa Raspberry o bien en un servicio en la nube como AWS o Azure. En el caso que presento, describiré los pasos que he seguido para utilizar NodeRED en un contenedor Docker.

Para realizar la descarga de la imagen de Docker con la instalación de NodeRED, ejecuté el siguiente comando:

docker pull nodered/node-red

Para realizar el arranque de NodeRED con la imagen descargada, ejecutaré el siguiente comando desde la línea de comandos:

docker run -it -p 1880:1880 -v node_red_data:/data --name mynodered nodered/node-red

Una segunda forma para arrancar NodeRED en Docker es identificando el volumen externo del contenedor con una carpeta del sistema de ficheros de la máquina en donde levanta. Para realizar este arranque, identificamos el path de la carpeta de la siguiente forma:

docker run -it -p 1880:1880 -v /path/to/folder:/data --name mynodered nodered/node-red

La descripción de los parámetros del comando anterior son las siguientes:

  • -it.- Modo de arranque del contenedor.
  • -p 1880:1880.- Acceso al interfaz gráfico del contenedor por el puerto 1880.
  • -v node_red_data:/data.- Creación de un volumen de datos a la carpeta de configuración de datos de NodeRED con el nombre /data.
  • –name mynodered.- Asignación del nombre del contenedor con nombre mynodered.
  • nodered/node-red.- Nombre de la imagen docker que se emplea en el contenedor.

Para verificar si el contenedor se ha levantado correctamente, se ejecuta el siguiente comando desde la línea de comandos:

docker container ps -a

Para acceder a la interfaz gráfica del editor de NodeRED una vez arrancado el contenedor, se abre una ventana de un navegador y se utiliza la siguiente URL: localhost:1880. El aspecto visual del editor queda representado en la siguiente imagen:

Figura1: NodeRED interfaz
Figura1: NodeRED interfaz

Ejemplo básico

Para definir un programa en NodeRED, se realiza la definición de un flujo de nodos en el panel de trabajo central con los nodos existentes en la paleta de nodos existentes en la parte izquierda del interfaz. El proceso se realiza seleccionando y arrastrando los nodos y enlazando dichos nodos.

Las categorías de los nodos en NodeRED son las siguientes:

  • Categoría common.- Nodos en donde definen funcionalidades comunes como; por ejemplo: inyección de un valor, operación de debug.
  • Categoría function.- Nodos en donde definen funciones de usuario, funciones de control de flujo,…
  • Categoría network.- Nodos en donde se definen operaciones de red; por ejemplo: peticiones MQTT o bien HTTP, entre otras.
  • Categoría sequences.- Nodos en donde se definen operaciones sobre secuencias de flujo.
  • Categoría parser.- Nodos en donde se definen operaciones de parseo de tipos de datos.
  • Categoría storage.- Nodos en donde se definen operaciones de almacenamiento.

Para desplegar y verificar el correcta definición, es necesario pulsar al botón «Deploy» ubicado en la parte superior derecha.

Un ejemplo básico tipo «Hola Mundo» consiste en inyectar un dato, como por ejemplo, el valor de una fecha determinada; procesar dicho valor en una función y, por último, mostrarlo por la consola debug.

El flujo se compondrá por los siguiente nodos: el primero, un nodo de tipo common de tipo inject el cual inyecta un valor; el segundo, un nodo tipo función el cual, el valor que recibe como parámetro de entrada, lo procesará y retornará para el siguiente nodo; y, por último, un nodo de tipo debug el cual realiza la escritura del valor inyectado.

Nodo inyector

El nodo inyector es aquel nodo que está compuesto por un objeto msg compuesto de dos atributos: payload, con el valor timestamp que se inyecta; y, topic, con el mensaje adicional a inyectar. El aspecto visual del componente es el siguiente:

Figura2: nodo inyector

Nodo Function

El nodo función es aquel nodo el cual gestiona distintos eventos los cuáles pueden ser: evento On Start, funcionalidad que se realiza al inicio del nodo; evento On Message, funcionalidad que se ejecuta al recibir un mensaje; y, evento On Stop, fncionalidad que se realiza al parar el nodo. Todo la funcionalidad se define en Java Script.

En nuestro ejemplo, se define la funcionalidad de evento al recibir un mensaje la cual realiza el tratamiento del valor timestamp recibido y su transformación en tipo String. El aspecto visual del componente es el siguiente:

Figura3: nodo function

Nodo Debug

El nodo debug es aquel que escribe en la consola el campo del mensaje especificado. El aspecto visual del componente es el siguiente:

Figura 4: nodo debug.

Para ejecutar el flujo de trabajo es necesario pulsar en el cuadro izquierdo ubicado en el nodo inject. El aspecto de la salida en la consola del ejemplo descrito es el siguiente:

Figura 5: nodo debug con resultado.

Obtención de la configuración y flujos de trabajo.

La definición de los flujos de trabajo que se definen de forma visual, representados en el fichero flows.json, así como la configuración de NodeRED, representado en el fichero settings.js, se encuentra en la carpeta data del contenedor y en el volumen de Docker. Una forma sencilla para realizar una copia de la carpeta data, es utilizando el comando docker de copiado. Así, para realizar una copia de la carpeta data del contenedor utilizado mynodered a una carpeta determinada, se puede emplear el siguiente comando:

docker cp  mynodered:/data  /your/backup/directory

En las siguiente entrada, NodeRED II: nodos principales, iré profundizando en la definición de flujos y usos de NodeRED.

Ejecución de Ansible en un contenedor Docker

Ansible es una herramienta open source, gestor de configuración, despliegue y orquestación. Su objetivo es automatizar tareas de IT para aumentar la productiviidad en tareas de configuración y despliegue. El mecanismos utilizado es la definición de playbooks con la declaración de las tareas.

Docker es un proyecto open source que permite el despliegue de aplicaciones en contenedores software en múltiples sistemas operativos.

Para trabajar con Ansible desplegando en un contenedor Docker es necesario definir un contenedor con un servicio SSH para que Ansible se pueda conectar y ejecutar los playbooks que se definen. Para realiizar esta tarea, necesitamos lo siguiente:

  1. Definir el contendor con el servicio SSH
  2. Configurar y definir el playbook de Ansible.

Docker

Lo primero que debemos de realizar es definir una Dockerfile con la configuración de la imagen del contenedor que contenga un servidor SSH. Para ello, nos basaremos en la documentación existente en la documentación de docker.

La modificación a realizar es insertar el valor de la password del usuario root. El resultado del fichero es el siguiente:

FROM ubuntu:16.04
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd
RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

Para realizar la construcción de la imagen, debemos de ejecutar el siguiente comando:

docker image build -t alvaroms/ubuntu-serverssh:v1 .

Para arrancar el contenedor con la imagen creada anteriormente, ejecutamos el siguiente comando:

docker container run -d --name dockerssh -p 50022:22 alvaroms/ubuntu-serverssh:v1

Una vez arrancado el contenedor, debemos de obtener la IP asignada; para ello, debemos de inspeccionar la configuración del contenedor para obtener la IP del Gateway; esta operación, la realizamos con el siguiente comando:

docker container inspect dockerssh | grep Gateway

Suponiendo que la Ip obtenida es la 172.17.0.1, la conexión ssh la realizamos con el siguiente comando:

ssh -p 50022 root@172.17.0.1

Para completar la operación, deberemos de introducir la password definida en el fichero Dockerfile.

Ansible

Para trabajar con Ansible he definido un usuario con nombre Ansible y los permisos adecuados.

En el fichero /home/ansible/inventory inserto las siguiente líneas:

[all:vars]
ansible_connection=ssh
ansible_user=root
ansible_ssh_pass=root
[...]
[dockers]
ubuntu-sshserver ansible_port=50022 ansible_host=172.17.0.1

Con estas modificaciones definimos las credenciales de conexión al contenedor Docker y definimos la configuración del contenedor para utilizarla en los playbook.

Definimos una playbook con el cual nos permite instalar Git. El playbook es el siguiente:

--- # install git on target host
- hosts: ubuntu-sshserver
become: yes
tasks:
- name: install git
yum:
name: git
state: latest

Para la ejecución del playbook ejecutamos el siguiente comando:

ansible-playbook -i /home/ansible/inventory ./git-setup.yml

Para verificar la correcta instalación, nos conectamos al contenedor por SSH y obtenemos la versión de git los comandos son:

ssh -p 50022 root@172.17.0.1
docker>git --version

Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose

Finalizamos la serie de artículos de Kotlin y SpringBoot con el presente artículo con título «Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose». El objetivo de la entrada es dockerizar la aplicación para hacer funcionar la aplicación desarrollada en un contenedor Docker. Además, definiremos la aplicación para que funcione en una composición de contenedores con un contenedor con MySQL.

La serie está compuesta de tres entradas ordenadas de forma cronológica:

La estructura del artículo está compuesto por los siguientes puntos:

  1. Docker
    • Propuesta 1
    • Propuesta 2
    • Propuesta 3
  2. DockerCompose

1.- Docker

La aplicación la podemos dockerizar de tres formas posibles. Las diferencia reside en el tamaño de la imagen resultante.

Propuesta 1

La estrategia consiste en copiar el contenido de la aplicación en el interior de la imagen y posteriormente realizar la creación del artefacto. El snippet con el contenido del Dockerfile es el siguiente:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN /app/gradlew build
RUN mv /app/build/libs/*.jar /app/app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v1 .

Para visualizar las imágenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de la ejecución del comando muestra una imagen con un tamaño aproximado a 686MB.

Propuesta 2

La segunda estrategía consigue reducir el tamaño de la imagen. Para ello, se realiza la creación del artefacto fuera de la imagen y se copia al interior de la imagen. Es decir, respecto a la solución anterior, se elimina el paso de construcción del artefacto. Para realizar la creación del artefacto, es necesario definir una tarea de creación del fichero jar mediante una tarea en Gradle. La tarea Gradle es la siguiente:

tasks.register("buildDocker"){
  dependsOn("build")
  doLast{
    fileList("./build/libs").forEach { file ->
      file.copyTo(File("./ejem1kotlindocker.jar"))
    }
  }
}

Como se observa en el snippet anterior, la tarea «buildDocker» tiene una dependencia con la tarea build; una vez ejecutado build, su resultado, generado en la carpeta «./build/libs» del proyecto, se copia a la carpeta raíz del proyecto. Esta tarea debe de ser ejecutada previamente a la construcción de la imagen.

La definición de la imagen en el Dockerfile es la siguiente:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
RUN mkdir /app
WORKDIR /app
EXPOSE 8080
ADD ./ejem1kotlindocker.jar /app/ejem1kotlindocker.jar
ENTRYPOINT ["java", "-jar", "/app/ejem1kotlindocker.jar"]

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v2 .

Para visualizar las imagenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de esta forma implica la creación de una imagen con un tamaño aproximado a 149MB. En relación a la primera forma, la reducción del tamaño es notable.

Propuesta 3

La tercera estrategia consiste en fusionar los objetivos de las anteriores. Para ello, primeramente, realizamos la creación del artefacto en una primera capa; posteriormente,
con el artefacto creado, construimos la imagen. Todo ello en el paso de construcción de la imagen y en elmomento de crear la imagen Docker. El snippet con el contenido del Dockerfile es el siguiente:

FROM openjdk AS build
VOLUME /tmp
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN /app/gradlew build
FROM openjdk:8-jdk-alpine

ARG APP_VERSION=1.0.0-SNAPSHOT
ENV database__client=mysql
ENV database__connection__host=mysql
ENV database__connection__user=root
ENV database__connection__password=password
ENV database__connection__database=prueba
ENV SPRING_PROFILES_ACTIVE=dev
LABEL org.label-schema.version=$APP_VERSION
COPY --from=build /app/build/libs/*.jar /app/app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar" ]

Con esta opción, definimos las variables de entorno con la configuración por defecto a la base de datos cuyos valores pueden ser pasados por parámetro en la creación de la imagen. De la misma manera que los argumentos necesarios.

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v3 --build-arg APP_VERSION=1.0.3-SNAPSHOT .

Para visualizar las imagenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de esta forma implica la creación de una imagen con un tamaño aproximado a 149MB. En relación a la primera forma, la reducción del tamaño es notable;y, respecto a la segunda, no es necesario realizar ninguna operación intermedia. Así, de las tres definiciones de Dockerfile, esta tercera, es la solución más eficiente.

2.- DockerCompose

DockerCompose permite realizar la composición de contenedores interconectados. La definición de la composición se realiza en el fichero docker-compose.yml, ubicado en la raíz del proyecto. La composición se define en base a dos servicios: el primero, mysql, corresponde con un contenedor de una imagen de MySQL; el segundo, kotlindocker, corresponde con la aplicación desarrollada.

Cada contenedor está definida en dos redes: kotlinnetwork, para el contenedor de la aplicación; y, mysqlnetwork, para el contenedor de la base de datos MySQL. La conexión la realiza el servicio de kotlin al tener conexión a las dos redes. Para los volúmenes, se definen un volumen para cada servicio.

Otro detalle importante, reside en qué imagen de la aplicación seleccionamos, o bien, creamos la imagen a partir del código existente: opción build, creamos la imagen a partir del código; e, image, creamos el contenedor a partir de una imagen definida.

El contenido del fichero docker-compose es el siguiente:

version: '3'
services:
  mysql:
    container_name: mysql
    image: mysql:5.7
    environment:
      - MYSQL_DATABASE=prueba
      - MYSQL_ROOT_PASSWORD=password
    ports:
      - 3306:3306
    volumes:
      - mysqlvolume:/var/lib/mysql
    networks:
      - mysqlnetwork
  kotlindocker:
    depends_on:
      - mysql
    container_name: kotlindocker
      # image: alvaroms/ejem1kotlindocker:v4
      build: ./
    environment:
      - SPRING_PROFILES_ACTIVE=dev
    ports:
      - 8087:8080
    volumes:
      - kotlinvolume:/var/lib/kotlin
    networks:
      - kotlinnetwork
      - mysqlnetwork
    volumes:
      kotlinvolume:
      mysqlvolume:
    networks:
      kotlinnetwork:
        driver_opts:
          com.docker.network.bridge.name: kotlinNetwork
      mysqlnetwork:
        driver_opts:
          com.docker.network.bridge.name: MySQLNetwork

Para el lector interesado en el código del proyecto puede acceder en el siguiente enlace

Finalizamos con una reflexión sobre la serie sobre kotlin, Spring Boot y Docker: un punto a tener en cuenta es que el código Kotlin presentado es muy mejorable al no haber aplicado patrones funcionales como los que se pueden presentar con la librería Arrow; con ella, la mejora del código es notable, por ejemplo, utilizando contenedores binarios.