Jupyter notebook en Raspberry Pi con Docker

Actualizado el 31 de Octubre de 2021

Docker

Introducción

Últimamente he estado usando Azure notebooks para mis notas en Jupyter pero parece que finalmente Microsoft ha decidido abandonarlo y da varias alternativas para la migración, pero ninguna me ha terminado de convencer, ya sea por estar atado a otro componente de Azure o por ser de pago, así que decidí incluir Jupyter dentro de una de mis Raspberry.

Uno de los problemas es la mala compatibilidad de Jupyter con Git, apenas existen extensiones que sean fiables así que decidí usar JupyterLab con su extensión de Git que es bastante completa.

Finalmente continuando con lo que parece que será la senda a seguir en un futuro cercano he optado a hacerlo en Docker, principalmente porque así lo puedo mover cómodamente entre mis Raspberry sin tener que instalarlo todo de nuevo (bueno, para lo que se supone que es Docker).

Estos son los pasos a seguir, en este caso use una Raspberry Pi 3 con 1 GB de RAM para comprobar el rendimiento, aunque Docker tiene más sentido en una Raspberry Pi 4 con al menos 4GB de RAM.

Configuración del sistema

Es recomendable leer este enlace antes de seguir, da instrucciones e ideas para instalar Docker.

DockerFile

Este es el fichero Dockerfile necesario para crear la imagen, vamos a usar como imagen base la versión ARM de Debian, posiblemente con Alpine el tamaño de la imagen sería menor (actualmente 1.2GB) pero puede haber algunos problemas de rendimiento.

# 0. Image and labels
FROM debian:latest
LABEL "guru.raraavis.creator"="blog@raraavis.guru"
LABEL "guru.raraavis.version"="1.1.0"
LABEL "guru.raraavis.release-date"="31/10/2021"
LABEL "guru.raraavis.description"="Raspberry Pi image with JupyterLab"

# 1. Environment vars
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_OPTIONS=--max-old-space-size=768
ARG TZ=Europe/Madrid
ENV NODE_VERSION=16
ENV JUPYTER_ID=1000
ENV TZ=$TZ

# 2. Install packages
# 2.1 Update system and install necessary packages
RUN     apt-get update && \
        apt-get install -y --no-install-recommends \
        python3-dev \
        python3-venv \
        libatlas-base-dev \
        libblas-dev \
        liblapack-dev \
        python-dev \
        gfortran \
        tzdata \
        software-properties-common \
        sudo \
        gosu \
        bzip2 \
        git \
        dumb-init \
        curl && \
        curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - && \
        apt-get install -y --no-install-recommends nodejs && \
        rm -rf /var/lib/apt/lists/* && \
        rm -rf /tmp/*
# 2.2 Install pip
ADD     https://bootstrap.pypa.io/get-pip.py get-pip.py
RUN     python3 get-pip.py
RUN     python3 -m pip config --global set global.extra-index-url https://www.piwheels.org/simple
# 2.3 Install pip packages and Jupyter
RUN     python3 -m pip install --upgrade \
        conda \
        virtualenv \
        ipykernel \
        jupyter \
        jupyterlab \
        jupyterlab-git \
        jupyter_contrib_nbextensions \
        autopep8

RUN     jupyter lab build
RUN     jupyter contrib nbextension install --system

# 3. Add jupyter user
RUN     adduser --uid $JUPYTER_ID --disabled-password --gecos '' jupyter
RUN     adduser jupyter sudo
RUN     echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

# 4. Install Miniconda
WORKDIR /home/jupyter
ADD --chown=jupyter     http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-armv7l.sh Miniconda3.sh
RUN     chmod 755 Miniconda3.sh
RUN     md5sum Miniconda3.sh
RUN     ./Miniconda3.sh -b -p /home/jupyter/miniconda3
RUN     ./miniconda3/bin/conda config --add channels rpi
RUN     rm ./Miniconda3.sh

# 5. Configure container
RUN     mkdir -p /home/jupyter/notebooks
RUN     chown -R jupyter:jupyter /home/jupyter
ONBUILD SHELL   ["/bin/bash","-c"]
VOLUME /home/jupyter/notebooks
EXPOSE 8888
USER jupyter

# 6. Execute jupyter
CMD ["jupyter", "lab", "--port=8888", "--no-browser", "--ip=0.0.0.0", "--allow-root", "--notebook-dir=/mnt/jupyter/notebooks", "--ServerApp.token=", "--ServerApp.password="]

Variables de entorno (1)

El paquete tzdata cuando se instala necesita especificar una zona horaria, así que establecemos la variable de entorno TZ, si se establece en tiempo de ejecución indicará la zona horaria a usar para configurar el sistema, se puede volver a configurar usando dpkg-reconfigure, tzdata también necesitará establecer DEBIAN_FRONTEND=noninteractive para evitar preguntas durante la instalación.
Enlazamos ARG con ENV, ya que ARG solo se puede usar en tiempo de compilación la enlazamos con ENV para que se pueda establecer también en tiempo de ejecución.
NODE_OPTIONS se usará más adelante para establecer parámetros en NodeJs, principalmente en el uso de la memoria, ya que a no ser que la Raspberry tenga más de 2GB fallará durante la creación de la extensión de Git, para una RPI4 se podría eliminar o ampliar este parámetro.
Existe también la opción de parametrizar que versión de Node se quiere instalar -necesario para JupyterLab- por defecto la 16 que es la última disponible actualmente.
La variable JUPYTER_ID se usa para indicar que id de usuario tendrá el usuario Jupyter que luego crearemos, esto es muy útil para poder establecer permisos en el host y evitar algunos problemas.

Instalar paquetes (2)

El proceso de instalación consta de un solo comando RUN para evitar crear capas intermedias. También crea e instala la extensión de Git, hay que tener en cuenta que no existe una forma oficial de generar la extensión y luego instalarla, de ser así una opción habría sido crearla en un host y luego copiarla en otro.
También se incluyen varias librerías de sistema, muchas de ellas son necesarias para instalar ciertos paquetes Python (principalmente librerías científicas) así que es bueno tenerlas ya instaladas.

Actualizar el sistema e instalar Jupyter (2.1)

Los comandos básicos para actualizar el sistema e instalar Jupyter con sus dependencias, se usa –no-install-recommends y finalmente se elimina de la cache los paquetes para aligerar el tamaño del fichero Docker.

Instalar pip (2.2)

Aquí descargamos e instalamos pip, también añadimos el repositorio de paquetes Python piwheels, este repositorio contiene muchos de los paquetes Python preparados para Raspberry lo que ahorra mucho tiempo de compilación e instalación, también indicamos que este repositorio sea global para que cualquier usuario o entorno virtual se beneficie del mismo.

Instalar librerías Python y Jupyter (2.3)

Aquí se instalan todos los paquetes Python necesarios para JupyterLab incluyendo algunos que son útiles -como virtualenv- extensiones a JupyterLab -como la propia de Git– y finalmente el propio Jupyterlab, después solo hay que llamar al comando build. Aprovechamos también para instalar algunas extensiones útiles para JupyterLab.

Agregar usuario Jupyter (3)

Ahora creamos el usuario jupyter que usaremos a partir de ahora, lo configuramos sudo y configuramos la carpeta de trabajo.

Instalar Miniconda (4)

En este paso vamos a instalar Miniconda para el usuario Jupyter, se hace una descarga e instalación desatendida dentro de la carpeta que hemos creado previamente. Miniconda instala principalmente el gestor de paquetes Conda y el resto de paquetes habría que instalarlos por separado, hay que tener en cuenta que el soporte de Conda para Raspberry no es muy bueno.
Finalmente se agrega el canal (repositorio) de rpi a conda para poder descargar paquetes orientados a Raspberry Pi (similar a como hemos hecho antes para pip).
Al instalar Conda de manera desatendida no se incluyen los binarios de Conda en el PATH por lo tanto para usar los binarios de conda hay que ubicarse en la carpeta bin de la carpeta miniconda como se ve al usar el comando conda config, esta opción la he preferido para no tener que usar la versión de Python de Conda que generalmente es inferior a la instalada en el sistema.

Contenedor (5)

Establecemos una carpeta de trabajo donde crearemos los notebooks y la usaremos también para clonar repositorios, establecemos los permisos adecuados para el usuario Jupyter.

También usamos ONBUILD para indicar que las imágenes que hereden de esta usarán bash como shell, esto es importante porque sino algunos comandos como source muy usados para crear entornos virtuales no estarán disponibles.

Añadimos la configuración de los puertos y la carpeta que se usará como volumen para mapear con el host que es donde residen los cuadernos.

Ejecutar Jupyter

Establecemos el comando que arrancará Jupyter, algunas consideraciones:

  • –no-browser: para evitar abrir el navegador al arrancar.
  • –allow-root: permite ejecutar Jupyter como root.
  • –notebook-dir: carpeta raíz de Jupyter, es decir de la que leerá los ficheros, que a su vez es la misma que usamos para Git.
  • –NotebookApp.token y NotebookApp.password lo establecemos con cadenas vacías para indicar que no queremos usar autenticación, esto para un entorno particular como suele ser el de una Raspberry es muy adecuado, en entornos compartidos quizás tenga más sentido no usar estos valores y establecerlos a través del archivo de configuración.

Compilación

Ejecutamos con el siguiente comando para compilar, previamente hacemos una limpieza para evitar posibles errores (errores GPG con las claves de los orígenes de los paquetes por ejemplo). Es posible que de algún error al compilar por falta de memoría, quitar algunos servicios o programas activos para ganar memoria y reintentar puede ayudar.

docker image prune -f
docker image build --tag user/image_name .

Ejecución

Una vez terminada la compilación con el siguiente comando arrancamos la imagen:

docker container run --init --publish 8888:8888 --detach --volume /home/pi/jupyter:/home/jupyter/notebooks user/image_name

Estamos mapeando el puerto 8888 al mismo en puerto en el host y también estamos mapeando la carpeta en Rasperry como un volumen en el host en la carpeta home del usuario pi, en id_imagen indicamos el id de la imagen a ejecutar. El parámetro –init es un valor recomendado para Jupyter que le proporciona más estabilidad.
Se pueden establecer también las variables de entorno con –env para JUPYTER_ID, NODE_VERSION o TZ o se pueden dejar los valores por defecto.

Push & Pull

Para subir la imagen solo tenemos que ejecutar el siguiente comando e introducir nuestras credenciales de Docker.

docker login

Para subir la imagen usamos la información del repositorio que hemos creado, normalmente usamos como user/name lo mismo que hayamos especificado en el tag que hemos creado con el comando docker build.

docker push user/image_name

Y para descargarlo en otra máquina simplemente hacemos:

docker pull user/image_name

Aunque si lo invocamos directamente con docker run tendrá el mismo efecto.

Configurar Git

Si arrancamos un navegador y vamos a la dirección de la Raspberry podremos ver el entorno de JupyterLab, vamos a configurar Git desde aquí.

La mayoría de las tareas básicas se pueden hacer desde los menús pero para comandos más avanzados podemos abrir un terminal y lanzar comandos desde ahí o para verificar que todo ha sido correcto, por ejemplo vamos a configurar Git con nuestro usuario e email, (nota, pegar se realiza con mayus+Insert).

git config user.email "coyote@acme.com"
git config user.name "El Coyote"

Para evitar que nos pregunte usuario y contraseña para los commit vamos a configurarlo también, desde el terminal hacemos:

git config credential.helper store
git pull

Introducimos el usuario y contraseña cuando lo pregunte, con esto quedarán almacenadas y no lo preguntará para el resto de acciones que hagamos desde la consola o desde la interfaz.

Ahora para clonar el repositorio usamos la interfaz, desde el menú Git -> Clone a Repository introducimos la URL del repo, nos preguntará por unas credenciales que tendremos que tener previamente configuradas en el servidor Git.

Esto nos descargará el código en la carpeta que hemos indicado, ahora podemos probar a crear cualquier archivo y realizar un commit, en la parte de la izquierda pulsemos el icono de Git y veremos los archivos modificados, seleccionamos los que queremos subir y en la parte inferior escribimos un resumen y una descripción opcional, a continuación pulsamos Commit, como ya hemos configurado el nombre y el correo en Git el commit no dará ningún error, sino que saldrá una ventana pidiendo esos datos.

Para hacer Push hay dos formas, se puede hacer directamente desde esta misma ventana usando el icono o desde el menú Git -> Push to remote.

Desde esta misma ventana también podemos cambiar de ramas o ver el historial.

(Opcional) Proxy Apache

Podemos usar Apache como proxy para acceder y así configurar estas URL por algo más fácil de recordar, para ello en Apache creamos un virtual host que nos haga de proxy con el servidor de JupyterLab, creamos un fichero en la carpeta /etc/apache2/sites-available/005-jupyterlab.conf con el siguiente contenido, obviamente deberemos tener un sistema DNS que nos resuelva correctamente el nombre jupyterlab, vamos a suponer que el servidor está en la dirección 192.168.1.4

Tenemos que habilitar algunos módulos, para permitir que funcionen los websockets y cors.

a2enmod headers
a2enmod proxy
a2enmod proxy_http
a2enmod proxy_wstunnel
a2enmod headers

Y ahora configuramos el servidor virtual

<VirtualHost spotify:80>
  ServerName jupyterlab.domain
  SererAlias jupyterlab
  Header set Access-Control-Allow-Origin "*"
  ProxyPreserveHost On
  ProxyPass / http://192.168.1.4:8888/
  ProxyPassReverse / http://192.168.1.4:8888/
  <Location "terminals/websocket">
      ProxyPass "ws://192.168.1.4:8888/terminals/websocket"
  </Location>
  <Location "/api/kernels/">
      ProxyPass "ws://192.168.1.4:8888/api/kernels/"
  </Location>
  CustomLog /var/log/apache2/jupyterlab.log combined
  ErrorLog /var/log/apache2/jupyterlab.error.log
</VirtualHost>

Para habilitar el sitio ejecutamos:

sudo a2ensite 005-jupyterlab.conf

Referencias

Configurar tzdata en Docker
Jupyter Notebook Dockerfile

Jupyter notebook en Raspberry Pi con Docker

Azure kubernetes

Pasos rápidos para crear un servicio kubernetes

  1. Creamos un recurso kubernetes service, a tener en cuenta
    1. Crear un grupo de recursos propios, facilita la gestión y la limpieza (usaremos kubernetes)
    2. Elegimos un nombre, se usará también como DNS (usaremos dummy)
    3. Elegir el tamaño de nodo adecuado, con la restricción de que tenga al menos 2 cores y 4GB de RAM (B2s estándar)
    4. Elegir el número de nodos
  2. Usamos la herramienta Azure CLI
    1. az login
      Logearse dentro de la plataforma
    2. az aks get-credentials –resource-group kubernetes –name dummy
      Actualiza el contexto kubernetes y se conecta al cluster, usando la herramienta de docker desktop se pueden ver todos los contextos de kubernetes activos.
    3. Se puede verificar usando el comando kubectl get nodes donde se pueden ver los nodos que habían sido creados
  3. A partir de este punto se pueden usar los comandos de kubernetes necesarios para desplegar como kubectl create

Creación de disco

Es muy común que se quiera crear un volumen de datos asociado a un cluster kubernetes, un ejemplo sería este:

kind: StorageClass 
apiVersion: storage.k8s.io/v1beta1
metadata: 
    name: azure-disk
provisioner: kubernetes.io/azure-disk 
parameters: 
    storageaccounttype: Standard_LRS 
    kind: Managed

En este caso no hace falta crear un volumen persistente porque Azure Disks soporta aprovisionamiento dinámico. Este es un ejemplo para una base de datos, hay que establecer la contraseña con:
kubectl create secret generic
mssql –from-literal=SA_PASSWORD=»Password_123″

apiVersion: v1 
kind: PersistentVolumeClaim 
metadata: 
    name: azure-volume-claim 
spec: 
    storageClassName: azure-disk 
    accessModes: 
        - ReadWriteOnce 
    resources: 
        requests: 
            storage: 8Gi
spec: 
    containers: 
        - name: myapp-database 
          image: mcr.microsoft.com/mssql/server 
          ports: 
              - containerPort: 1433 
          env: 
              - name: "ACCEPT_EULA" 
                value: "Y" 
              - name: "SA_PASSWORD" 
                valueFrom: 
                    secretKeyRef: 
                        name: mssql 
                        key: SA_PASSWORD 
              - name: "MSSQL_PID"
                value: "Express" 
          volumeMounts: 
              - name: mssqldb 
                mountPath: /var/opt/mssql 
    volumes: 
    - name: mssqldb 
          persistentVolumeClaim: 
              claimName: azure-volume-claim

Podemos crear también un balanceador para acceder a la base de datos.

apiVersion: v1 
kind: Service 
metadata: 
    name: myapp-db-svc 
spec: 
    selector: 
        app: myapp-db 
    ports: 
        - protocol: TCP 
          port: 1433 
          targetPort: 1433 
    type: LoadBalancer

Este script habilitará una IP externa que se puede usar para acceder a la base de datos, cuando no se necesite acceder se puede eliminar este balanceador.

Azure kubernetes

Kubernetes

En el caso de Kubernetes el nodo maestro no ejecuta ningún contenedor, (no es como swarm) solo tiene servicios que son necesarios para la infraestructura de Kubernetes. En el caso de Kubernetes se usan pods, un pod es la mínima unidad de trabajo de Kubernetes que contiene uno o más contenedores.

Kubernetes tiene este ciclo de vida para los pods:

  1. Pending: Se cargan los contenedores
    1. Failed: Error durante la carga
    2. Running: Todos los contenedores creados, al menos uno está funcionando.
      1. Succeded: Todos los contenedores en el pod han terminado correctamente.

Servicios

Kubernetes tiene varios servicios ejecutándose que interesa conocer, unos se ejecutan en el nodo maestro y otros son específicos del resto de nodos.

Maestro

kube-apiserver: Servidor REST API que consume datos JSON a través de archivos de manifiesto para la configuración de Kubernetes.

etcd: Almacenamiento clave/valor para pequeñas cantidades de datos en memoria, una parte principal de kubernetes que se recomienda guardar.

kube-controller-manager: Observa cambios en el sistema y reacciona para asegurar la configuración del sistema.

kube-scheduler: Asigna trabajo a los nodos e inspecciona el servidor API para la creación de nuevos pods.

Nodos

kubelet: Un agente que se comunica con el maestro para arrancar operaciones en el nodo e informar del estado del nodo.

kube-proxy: Componente que habilita el direccionamiento IP en el pod y balancea el tráfico a través de todos los pods del servicio.

Comandos

  • kubectl create -f pod.yml
    Crea un pod en base a un fichero
  • kubectl create -f service.yml
    Crea un servicio en base a un fichero yml
  • kubectl create -f deployment.yml
    Crea un deployment a partir de fichero
  • kubectl create secret generic secreto –from-literal=PASSWORD=»FooPass» –from-file=./<filename> –namespace=app
    Crea un secreto
    • –from-literal lo crea desde el literal de texto indicado
    • –from-file para colocar secretos en ficheros
    • –namespace si se ha creado un namespace para agrupar se puede especificar aquí
  • kubectl create namespace app
    Crea un espacio de nombre para agrupar a todos los elementos del cluster, también es útil para separar tus proyectos y limitar los recursos disponibles para cada uno de ellos. Es como tener un cluster virtual sobre uno físico. El namespace también sirve como DNS servicename.namespace.svc.cluster.local
  • kubectl describe pod
    Muestra toda la información disponible sobre el pod y los contenedores que incluye.
  • kubectl describe service app-rc-service
    Información sobre el servicio
  • kubectl describe deployment app-dpl
    Muestra información sobre el deployment
  • kubectl describe hpa
    Muestra las métricas en el cluster local
  • kubectl get pods
    Permite ver los pods creados y su estado
  • kubectl get pod
    Recupera el nombre del pod, útil para poder referenciarlo luego con otros comandos
  • kubectl get deployment dpl
    Permite ver información sobre el deployment creado, nombre, replica, pods actuales, acutalizados, disponibles…
  • kubectl get persistentvolumes
    Devuelve todos los volúmenes
  • kubectl get persistentvolume volumen
    Devuelve solo el volumen indicado
  • kubectl get pv
    Devuelve información sobre los volúmenes indicando cuales han sido reclamados y cuales siguen disponibles
  • kubectl get persistentvolumeclaims
    Devuelve todos los volúmenes reclamados
  • kubectl get persistentvolumeclaim volumen
    Devuelve solo el volumen reclamado indicado
  • kubectl get secrets
    Saca un listado de los secretos creados
  • kubectl get secret secreto -o yaml
    Devuelve el valor de un secreto
  • kubectl get namespaces
    Obtiene los espacios de nombre disponibles
  • kubectl get hpa
    Sirve para monitorizar HPA
  • kubectl get nodes
    Muestras los nodos del cluster
  • kubectl get service app-svc –watch
    Muestra información de un servicio
    • –watch monitoriza el comando hasta que se asigne una IP pública
  • kubectl run pod –image=autor/imagen –port 80 –restart=Never
    Crea un pod y lo arranca
    • –image la imagen que se usará para el pod
    • –port en que puerto estará escuchando
    • –restart indica que hacer en caso de reinicio, en este caso Never indica que no se recreará, falla o muere.
  • kubectl delete pod pod
    Borra un pod
  • kubectl delete deployment dpl
    Borra el deployment indicado
  • kubectl -n kube-system get secret
    Muestra todos los secretos del cluster. En general interesa el secreto que empieza por deployment-controller-token
  • kubectl -n kube-system describe secret deployment-controller-token-xyz
    Obtener un token
  • kubectl rollout status deployment app-deployment
    Permite ver el progreso de una operación rollout
  • kubectl rollout history deployment app-deployment
    Si se uso el parámetro –record al hacer un kubectl apply permite ver la historia del deployment
  • kubectl rollout undo deployment app-deployment –to-revision=1
    Permite volver a una versión específica
    • –to-revision versión a la que volver
  • kubectl expose rc app-rc –name=app-rc-service –type=NodePort
    Servicio que expone el pod a otros pods o clientes.
    • –name indica el nombre del servicio
    • –type indica el tipo de servicio
      • ClusterIP: Valor por defecto que expone el pod interno al cluster
      • NodePort: Expone el pod con un puerto estático en cada nodo que contiene el pod
      • LoadBalancer: Expone el pod externamente usando un balanceador de carga de un proveedor de cloud
      • ExternalName: Disponible desde la versión 1.7 de kube-dns para exponer los pods a través de los contenidos de el campo externalName
  • kubectl cluster-info
    Muestra información de estado del cluster.
  • kubectl proxy
    Arranca el servidor proxy de Kubernetes
  • kubectl apply -f deployment.yml –record
    Aplica los cambios hechos en el fichero
    • –record Sigue todos los cambios hechos durante el deployment, permite hacer un rollback a una versión previa de la aplicación.
  • kubectl exec -it dpl/bin/bash [–container container]
    Ejecuta un comando
    • -it dpl el deployment que se usará
    • comando el comando a ejecutar
    • –container el container donde se va a ejecutar
  • kubectl cp fichero dpl:fichero
    Copia el fichero al deployment indicado con el nombre dado en fichero
  • kubectl config view
    Muestra información de la configuración
  • kubectl autoscale deployment app-deployment –cpu-percent=50 –min=1 –max=5 –horizontal-pod-autoscaler-sync-period=x
    Este comando configura Horizontal Pod Autoscaler (HPA) , es un controlador que comprueba los recursos definidos en una configuración cada 15 segundos por defecto y los intenta implementar.
    • app-deployment Nombre del deployment a escalar
    • –horizontal-pod-autoscaler-sync-period=x intervalo entre refresco de configuración

Scripts

Ficheros pod

Pod

Ejemplo para crear un pod, generalmente no se usa mucho porque no se querrá tener solo un pod, es más probable querer crear un deployment (ver más abajo) que tiene más características como ReplicaSet y proporciona alta disponibilidad y escalado así como capacidades de monitorización.

apiVersion: v1 # version de la sintaxis del fichero
kind: pod # se va a crear un pod
metadata: 
    name: web-pod # nombre del pod
    labels: # agrega etiquetas personalizadas
        app: app
        zone: prod 
        version: v1 
    spec: # definición del pod
        containers: 
           - name: app-web
             image: autor/imagen
             ports: 
                 - containerPort: 80

ReplicationController

Ejemplo para ReplicationController, necesario cuando se quieren replicar pods específicos, si ya existiera uno del tipo especificado se tiene en cuenta para el total.

apiVersion: v1 
kind: ReplicationController # Tipo necesario para ReplicationController
metadata: 
    name: app-rc 
    spec: 
        replicas: 5 # número de replicas
        selector: 
            app: app # pod a replicar
            zone: prod
            version: v2  
        template: # plantilla con información sobre el pod
            metadata: 
            labels: 
                app: app 
                zone: prod 
                version: v1 
            spec: 
                containers: 
                    - name: app-web
                      image: autor/imagen
                      ports: 
                      - containerPort: 80

Servicio

Script para crear un servicio (kubectl expose rc)

Normalmente un fichero deployment y otro service será lo necesario para crear un cluster kubernetes.

apiVersion: v1 
kind: Service 
metadata: 
    name: app-svc 
spec: 
    selector: 
        app: app # que pods será expuestos
    type: NodePort # diferentes tipos (ver kubectl expose rc) 
    ports: # mapeo de puertos
    - port: 80 # puerto interno
        nodePort: 30001 # puerto del cluster

El selector en este caso está usando una etiqueta, por lo que conectará todos los pods con esa etiqueta, las etiquetas se pueden establecer en un ReplicationController en la sección label la restricción es que se realiza una comparación por igualdad.

Deployment

Fichero para crear un deployment, opción recomendada para generar un cluster.

apiVersion: apps/v1 
kind: Deployment 
metadata: name: app-deployment 
spec: 
    replicas: 3 
    selector: 
        matchLabels: 
            app: myapp 
        matchExpressions:
            - {key: zone, operator: In, values: [prod, test]} # ReplicaSet
        strategy:
            type: RollingUpdate
            minReadySeconds: 5 # Segundos a esperar antes de crear el siguiente pod
            rollingUpdate:
                maxSurge: 1 # % o # de pods que pueden exceder las réplicas solicitadas
                maxUnavailable: 1 # % o # de pods que pueden estar no disponibles durante la actualización
    ports:
       - protocol: TCP
         port: 1433
         targetPort: 1433
    type:  ClusterIP                 
    template:  
        metadata: 
            labels: 
                app: myapp 
                zone: prod 
                version: v1 
        spec: 
            containers: 
                - name: app-web 
                  image: apomic80/myapp:frontend-v1 
                  ports: 
                      - containerPort: 80
                  env:
                      - name: "Environment"
                      - value: "Production"
                      - name: "PASSWORD"
                        valueFrom:
                            secretKeyRef:
                                name: secreto
                                key: Password   
                  volumeMounts: # volúmenes a montar
                      - name: mountVolume # volumen donde será montado
                        mountPath: ruta # ruta que será montada
                      - name: secret-volume
                        mountPath: /etc/secretVolume 
            volumes: # volúmenes reclamados a mapear en el host
            - name: mountVolume # volumen lógico a ser usado por los contenedores
              persistentVolumeClaim:
                  claimName: volume-claim # este volumen se tiene que haber creado previamente
            - name: secret-volume # volumen lógico que tb es secreto
              secret:
                  secretName: mssql # el nombre del secreto para poder acceder al volumen

Un deployment por defecto creará un ReplicaSet para poder indicar expresiones más complejas en las etiquetas, como si está contenida en un conjunto.

Hay dos estrategias de actualización:

  • Recreate: Quita la versión previa y carga una nueva, útil para escenarios de desarrollo o prueba
  • RollingUpdate: El valor por defecto, se mueve a una nueva versión gradualmente basado en los parámetros configurados, útil para entornos productivos

PersistentVolume

Un PersistentVolume es un recurso creado por un administrador de cluster con características específicas y un ciclo de vida independiente de tus aplicaciones

apiVersion: v1 
kind: PersistentVolume 
metadata: 
    name: local-volume 
    labels: 
        type: local 
spec: 
    storageClassName: hostpath 
    capacity: 
        storage: 10Gi # tamaño del almacenamiento
    accessModes: 
        - ReadWriteOnce # modo de acceso lectura/escritura
    hostPath: 
        path: /mnt/data # ruta donde se montará el volumen

Normalmente una ruta local para el almacenamiento solo se usa por razones de prueba o desarrollo.

PersistentVolumeClaim

PersistentVolumeClaim es un almacenamiento solicitado por el usuario basado en lo que necesita, como modos de acceso específico o tamaños.

El almacenamiento se crea con un fichero PersistentVolume con ciertas características y con un fichero PersistentVolumeClaim se solicita uno que tenga las características que quiere el usuario.

apiVersion: v1 
kind: PersistentVolumeClaim 
metadata: 
    name: local-volume-claim 
    spec: 
        storageClassName: hostpath 
        accessModes: 
            - ReadWriteOnce # tipo de modo de acceso solicitado
        resources: 
            requests: 
                storage: 3Gi # tamaño solicitado

Secret

Es posible crear un fichero que solo contenga un secreto que se usará para almacenar contraseñas por ejemplo.

apiVersion: v1 
kind: Secret 
metadata: 
    name: secreto
type: Opaque 
data: 
    PASSWORD: UGFzc3dvcmRfMTIz # tiene que estar en base 64

En Linux se puede pasar un literal a Base64 como:
echo -n Password_123 | base64
Y para decodificarlo:
echo -n UGFzc3dvcmRfMTIz | base64 –decode

ResourceQuota

Establecer cuotas de recursos

apiVersion: v1
kind: ResourceQuota
metadata:
    name: myapp-quota
spec:
    hard:
        limits.cpu: 1 # Cantidad de CPUs que pueden usar
        limits.memory: 2Gi # Cantidad de memoria que pueden usar

Kubernetes

Docker swarm

Uno de los dos sistemas de clustering más famosos para Docker, con docker swarm las máquinas se denominan nodos y se manejan a través de un conjunto de comandos docker node.

Los contenedores no se ejecutan en swarm se trabaja a un nivel más alto que se llama servicios. Arrancar un servicio es como arrancar un contenedor, pero un servicio puede arrancar múltiples instancias de un contenedor.

Una réplica es la instancia de un contenedor, puede haber varias réplicas arrancando desde la misma imagen con la misma configuración.

Comandos

  • docker swarm init –advertise-addr w.x.y.z –listen-addr w.x.y.z
    Arranca el host (nodo) en formato swarm y lo convierte en manager usando la dirección IP w.x.y.z (solo necesario si el host tiene varias IP) en esta dirección es donde estará escuchando peticiones, útil para escuchar peticiones desde el exterior en un entorno multicloud
    • La dirección IP actual donde donde debería estar escuchando el manager
  • docker swarm join –token token host:port
    Une el host (nodo) al swarm indicado en host:port, el token se obtiene a través de docker swarm init
  • docker service create –name nombre –publish 80:80 autor/imagen –constraint ‘node.labels.cpu != armhf‘ –replicas –network web-net 3 user/image
    Arranca un servicio en swarm
    • –constraint establece una restricción, en este caso en base a un tag y no arrancará nodos con ese tag
    • –replicas indica el número de replicas a ejecutar
    • –network red a la que asociarse, creada usando docker nework create
  • docker service ls
    Indica que servicios están arrancados
  • docker service ps [servicio]
    Indica que servicios están arrancado que contenedores
    • Si se indica servicio se muestra la información de la máquina que lo está ejecutando de cara a poder acceder a ella
  • docker service scale contenedor=5
    Agrega cuatro replicas del contenedor a swarm.
  • docker node update –label-add [cpu=armhf] [raspberrypi]
    Inserta una etiqueta cpu=armhf en el nodo indicado raspberrypi
  • docker network create -d overlay net01
    Creación y opciones de creación de red
    • -d overlay indicar el tipo de red, normalmente es bridge, overlay es necesario para varios nodos usando swarm
  • docker node ls
    Permite ver todos los nodos y su estado actual
Docker swarm

Docker-machine

Docker Machine permite aprovisionar máquinas Docker en una variedad de entornos, como máquinas virtuales que residen en la máquina local, en la nube. Docker Machine crea un host Docker, y usando el cliente Docker Engine para construir imágenes y crear contenedores en el host.

Comandos

  • docker-machine create –driver digitalocean –digitalocean-access-token=[YOUR-API-TOKEN] –digitalocean-region lon1 –digitalocean-image ubuntu-16-04-x64 do-node-01
    Crea una nueva VM («droplet» en DigitalOcean)
    • –digitalocean-region lon1 indica la región de Londres
    • –digitalocean-image ubuntu-16-04-x64 la imagen a cargar
  • docker-machine env [-u]
    Información adicional de los nodos creados
    • -u resetea las variables de entorno y redirige CLI al host local
  • docker-machine ls -q
    Mostrar todas las vm creadas
  • docker-machine ip
    Muestra la IP de Docker VM

Scripts

  • eval $(docker-machine env do-node-01)
    • Permite conectar al servidor Docker Engine y trabajar con la VM Digital Ocean

Docker-machine

Docker compose

Comandos

  • docker-compose up [-d] [–scale servicio=x]
    Crea los contenedores y los arranca debe existir en la carpeta un fichero docker-compose.yml
    • -d ejecuta como un demonio manteniéndolos arrancados en segundo plano
    • –scale se indica el servicio y un número y ejecutará tantos containers como los indicados
  • docker-compose stop
    Para todos los contenedores
  • docker-compose start
    Arranca los contenedores en docker compose
  • docker compose down
    Elimina los contenedores y cualquier red que se haya creado entre ellos

Docker compose file

Formato yml, por convención se llama docker-compose.yml

version: '3' # version del fichero docker
services: # servicios asociados
    web: # servicio que se ejecutara
        image: autor/imagen # imagen asociada
        depends_on: # depende de este contenedor, lo ejecutará antes
           - contenedor_1
        ports:
           - 5000:5000 # puertos a abrir
        volumes:
           - ./carpeta_host/archivo: /carpeta_contenedor/archivo
        environments:
           PASSWORD: "FooPass" 
           LOGIN: "Foo"
        context: ./context # al no usar el nombre por defecto
        dockerfile: prod.dockerfile # no se usa el nombre por defecto
    util:
        build: ./folder  # carpeta donde se generará el servicio
        image: autor2/imagen2
        container_name: nombre # nombre de la imagen
        command: echo google # comando que se ejecutará al arrancar     
  • Si no se especifica container_name se cogerá por defecto la concatenación del nombre de la carpeta con el nombre del servicio y el número de instancia
Docker compose

Docker

Actualizado el 28 de Noviembre de 2020

Comandos

Comandos básicos para manejar docker agrupados por funcionalidad.

  • docker container run [–interactive|-it] [–tty|-t] [–rm] [–volume|-v v1:v2] [–volumes-from contenedor:ro] [–detach|-d] [–publish|-p p1:p2] [–name nombre] [–network net01 | –net net01] [-e argumento] [–link container] imageid
    Arranca un contenedor, hello-world es un contenedor por defecto que se suele usar para verificar que todo está bien, este comando es el mismo que docker run, ahora es docker comando subcomando por una nueva sintaxis
    • id identificador del contenedor, se pueden usar solo los primeros números
    • –detach hace que el contenedor arranque como un proceso separado en lugar de en la propia consola
    • –publish x:y esto permite abrir el puerto p1 en el host conectado al puerto p2en el contenedor
    • –interactive arranca en modo interactivo emulando un terminal
    • –tty emulador de terminal a usar (se suele usar con –interactive), útil para ver procesos ejecutándose y poder pararlos
    • –rm borra el contenedor al terminar
    • –volume monta el volumen v1 en el host usando la carpeta v2 en el contenedor
    • –volumes-from permite montar un volumen a partir de otro contenedor
    • –name asigna un nombre al contenedor, registrado en el DNS interno de docker
    • –network asocia el contenedor a la red indicada
    • -e permite especificar argumentos que se pasarán al contenedor, por ejemplo -e ‘PASSWORD=FooPass’
    • –link enlaza el contenedor a otro, de esta forma el contenedor podrá comunicarse con el otro usando su nombre (es un argumento descartado que ya no se usa)
  • docker container ls [–all|-a]
    Saca un listado de todos los contenedores en ejecución, docker container ps, docker ps son otros alias para este comando
    • El parámetro –all saca todos los contenedores incluso los detenidos
  • docker container exec [–interactive|-it] [-u #] [contenedor] comando
    Arranca un comando dentro del contenedor y devuelve la salida al host
    • contenedor ejecuta la orden en el contenedor indicado
    • –interactive arranca en modo interactivo emulando un terminal
    • -u para ejecutar con un usuario concreto, por ejemplo 0 sería para root.
      • Este comando permite ejecutar docker como root
        docker container exec -u 0 -it contenedor bash
  • docker container inspect imagen
    Da información adicional sobre el contenedor, como por ejemplo los volúmenes en uso o la dirección IP asociada
  • docker container stop contenedor
    Para el contenedor, es un alias para docker stop
  • docker container start contenedor
    Arranca un contenedor creado previamente con docker run, es un alias para docker start
  • docker container kill contenedor
    Fuerza el contenedor a cerrarse
  • docker container rm [-v] contenedor
    Borra el contenedor, siempre que no esté funcionando, es un alias para docker rm
    • -v también elimina el volumen asociado al contenedor
  • docker container logs contenedor
    Muestra los logs de contenedor (por defecto usa el comando logs)
  • docker image build [–file server.dockerfile|-f server.dockerfile] [–tag user/image] .
    Construye la imagen usando un fichero dockerfile por defecto usará como fichero uno llamado dockerfile (esa es la razón de .) es un alias para docker build
    • –file fichero el fichero de configuración que se usará para construir la imagen
    • –tag user/image la etiqueta de la imagen que se va a usar para construir el contenedor
  • docker image history dockersuccinctly/a
    Muestra el historial de las capas de la imagen
  • docker image pull user/image
    Se descarga la imagen indicada
  • docker image tag user/image server:5000/dockerregistry/server
    Crea un tag server:5000/dockerregistry/server para la imagen user/image necesario para subir imágenes a un repositorio o referenciarlas de una forma más cómoda, es un alias para docker tag
  • docker image push localhost:5000/dockerregistry/server
    Sube la imagen al registro especificado
  • docker image rm imagen
    Borra la imagen imagen
  • docker image ls | docker images
    Muestra todas las imágenes en la caché local es lo mismo que usar docker images
  • docker network create net01
    Create crea un puente de red llamado ch05
  • docker network ls
    Muestra todas las redes
  • docker create imagen
    Se crea el contenedor pero no se arranca, acepta los mismos parámetros que docker run
  • docker version
    Devuelve la versión de Docker
  • docker login
    Permite validarse en docker.com
  • docker volume ls -qf dangling=true
    Saca todos los volúmenes dangling básicamente volúmenes huérfanos que no se han borrado al cerrar el contenedor o que no están siendo referenciados por ningún contenedor activo
  • docker system prune
    Borra contenedores sin usar, volúmenes e imágenes

Se puede asociar un nombre al contenedor, este nombre se ve al usar un comando ls, sino se especifica se crea un nombre aleatorio.

En lugar de usar un nombre para contenedor/imagen se puede usar el id de la imagen o el contenedor, también en particular solo con los primeros valores del id es suficiente (sino hay coincidencia).

Scripts

Es posible ejecutar algunos comandos que pueden ayudar en la ejecución de ciertos comandos, por ejemplo:
$(pwd)/src:/app/src
$(pwd) devuelve la ruta y la anexa al resto de la ruta

Estos son otros ejemplos útiles que se pueden ejecutar en Linux

  • docker container rm $(docker container ls -a -q)
    Elimina todos los contenedores que están parados
  • docker volume rm $(docker volume ls -qf dangling=true)
    Borra todos los volúmenes dangling
  • docker image rm $(docker image ls -f «dangling=true» -q)
    Borra imágenes que no se usan

Docker file

Como nota existe un fichero .dockerimage que se puede usar si se quiere evitar que se copien ciertas carpetas, una idea similar a .gitignore.

Este es un resumen de un fichero docker file, un resumen más amplio aquí:

ComandoDescripción
FROM x [AS y]Empieza desde la imagen x y le asigna el nombre y
RUN yEjecuta el comando y en el contenedor, se puede especificar un shell concreto, p.ej:
RUN [«/bin/bash», «-c», «source env/bin/activate»]
SHELL [«»,»»]Cambia el shell de ejecución al indicado, se usará este para todas las órdenes subsiguientes hasta que se cambie, p. ej: SHELL [«/bin/bash», » -c»]
ARG x yEstablece un valor que se puede usar en tiempo de compilación (equivalente a export) se puede usar en otras instrucciones mediante $x
ENV x yEstablece la variable de entorno x con el valor y esta se puede establecer en tiempo de ejecución por línea de comandos mediante el parámetro –env (–env X=Y)
COPY x y … zCopia archivos desde el contexto (carpeta del host desde la que se están ejecutando docker) a la imagen
EXPOSE xExpone el puerto x de tal forma que pueda ser mapeado en el host
VOLUME xCrea un directorio dentro de la imagen que pueda ser mapeado a un almacenamiento externo
CMD xEspecifica el comando a ejecutar x cuando arranca el contenedor, en Windows sería [«powershell», «Write-Host ‘hello world!'»]
WORKDIR xEstablece la carpeta de trabajo, todos los comandos que se ejecuten irán referidos a esta carpeta
ENTRYPOINT xEjecuta un script, la opción recomendada cuando se quiere usar el contenedor como un ejecutable
Instrucciones docker file

Referencias

Docker exec as root

Docker

Compilaciones deterministas

Una buena característica que se puede añadir a una librería además de SourceLink o crear la documentación es configurar la compilación como determinista.

Esto quiere decir que compilar ensamblados bajo las mismas condiciones de entrada producirá el mismo binario equivalente byte a byte. Por condiciones de entrada se entienden:

  • La secuencia de parámetros de entrada al compilador (flags).
  • Los contenidos del archivo de respuesta del compilador .rsp.
  • La versión exacta del compilador usado así como de los ensamblados referenciados.
  • Ruta completa del directorio actual (se pueden abreviar usando relativas)
  • Contenidos binarios de todos los ficheros pasados al compilador
    • Código fuente
    • Ensamblados referenciados
    • Módulos referenciados
    • Recursos
    • El nombre del fichero de claves (strong name)
    • Archivos de respuesta @
    • Analizadores
    • Conjuntos de reglas
    • «archivos adicionales» que puedan ser usados por analizadores
  • La cultura actual (se refiere a la cultura en la que se producen los mensajes de excepción y diagnóstico)
  • La codificación por defecto (o la actual) si la codificación no se ha especificado
  • La existencia, o no existencia, y contenidos de archivos en las rutas de búsqueda del compilador ( p.ej /lib, /recurse)
  • La plataforma CLR en la cual el compilador funciona (esto se nota por ejemplo al calcular precisiones en algunos tipos numéricos)
  • El valor de %LIBPATH% ya que esto puede afectar la carga del analizador de dependencias
  • Ahora mismo el compilador también depende de la hora del día y números aleatorios generados para GUID que no es determinístico si no se especifica /deterministic.

Permitir que el binario resultante sea el mismo proporciona varias ventajas por ejemplo para mecanismos de caché y optimización de test. También asegura que tanto la herramienta de compilación como la forma en que se ha compilado es consistente y verificable tanto por si misma con respecto al código fuente utilizado.

De igual forma en compilaciones incrementales incrementa la rapidez de compilación al compilar solo algunas partes, por ejemplo los ficheros de salida que ya están actualizados respecto a los ficheros de entrada no son ejecutados.

En el mundo DevOps proporciona ciertas ventajas para saber que pasos de compilación que dependen de cambios en un binario tienen que ser ejecutados.

La opción determinista está habilitada por defecto desde VisualStudio 2017, si se quiere habilitar para una versión anterior o deshabilitarla, tendríamos que editar el fichero .csproj.

<PropertyGroup>
  <Deterministic>true</Deterministic>
</PropertyGroup>

Es posible que se quiera deshabilitar ya que la opción determinista no permite establecer un número de versión autoincremental (*) por razones obvias este valor cambia en cada compilación por lo que no es determinista.

Este valor también se puede pasar al compilador a través del flag /deterministic

DevOps

Los PDBs contienen las rutas de los ficheros que se usarán para depurar, esto en entornos locales no es un problema, pero en entornos DevOps puede ser un problema al no tener acceso a esas rutas o incluso a la máquina, para esto existe la directiva <DeterministicSourcePaths/> que debe ser establecida en true, esto hace que la compilación sea completamente determinista tanto en local como en entornos DevOps.

Configuración

Con todo lo anterior lo que habría que hacer es establecer las directivas <Deterministic/> y <ContinuousIntegrationBuild/> con los valores comentados.

Además hay que incluir la directiva <EmbedUntrackedSources /> para que se incluya también el código generado por el compilador como AssemblyInfo.cs, hay que tener en cuenta que si ya está configurado para SourceLink entonces esta directiva ya estará incluida.

Además para entornos DevOps hay que añadir una configuración adicional.

Azure DevOps

<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
  <Deterministic>True</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

GitHub

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
  <Deterministic>True</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

Si se está usando un target de SDK inferior a 3.1.300 hará falta agregar un fichero Directory.Build.targets con la siguiente configuración, también hará falta para librerías .NetStandard (por lo menos hasta la versión 2.1 incluida).

<Project>
  <PropertyGroup>    
    <TargetFrameworkMonikerAssemblyAttributesPath>
$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 
    </TargetFrameworkMonikerAssemblyAttributesPath>
  </PropertyGroup>
  <ItemGroup>
    <EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
  </ItemGroup>
</Project>

Si se está usando Coverlet entonces habrá que usar la siguiente configuración para no tener problemas durante la integración continua.

<Project>
  <PropertyGroup>    <TargetFrameworkMonikerAssemblyAttributesPath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)'))</TargetFrameworkMonikerAssemblyAttributesPath>
  </PropertyGroup>
  <ItemGroup>
    <EmbeddedFiles Include="$(GeneratedAssemblyInfoFile)"/>
  </ItemGroup>
  <ItemGroup>
    <SourceRoot Include="$(NuGetPackageRoot)" />
  </ItemGroup>
  <Target Name="CoverletGetPathMap"
          DependsOnTargets="InitializeSourceRootMappedPaths"
          Returns="@(_LocalTopLevelSourceRoot)"
          Condition="'$(DeterministicSourcePaths)' == 'true'">
    <ItemGroup>
      <_LocalTopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"/>
    </ItemGroup>
  </Target> 
</Project>

Mapear rutas

Si no se compila en formato portable es posible que las rutas dentro de los ficheros PDB no sean correctas o cambien, es posible mapear la ruta física respecto a la ruta que aparece en los PDB mediante la directiva PathMap, como:

<PropertyGroup>
  <Deterministic>true</Deterministic>
  <PathMap>$(EnlistmentRoot)=C:\</Features>
</PropertyGroup>

Siendo EnlistmentRoot la variable que apunta al raíz del código fuente, el parámetro del compilador sería:

-pathmap:path1=sourcePath1,path2=sourcePath2

Verificar

Para probarlo localmente se puede compilar pasando la propiedad TF_BUILD al compilador (si es orientado a Azure DevOps), por ejemplo:

dotnet build /p:TF_BUILD=true

Para comprobar que todo está correcto, después de compilar empaquetamos, usando por ejemplo la opción pack del proyecto y al abrirlo con NuGet Package Explorer deberíamos ver el tick verde en Deterministic.

Esto tiene una ventana añadida, podemos verificar que el paquete es correcto antes de aplicarlo a un pipe DevOps.

Referencias

Ventajas de compilaciones deterministas
Compilaciones deterministas en Roslyn
Deterministic source paths
Compilaciones incrementales
Entradas deterministas
Problemas con versionado automático
Configuración Deterministic Build
Configuración PathMap
Opción del compilador (-pathmap)
Opción del compilador (-deterministic)
Portable PDB

Compilaciones deterministas

USB externo en Raspberry Pi

Actualizado el 22 de Noviembre de 2020

Una forma rápida y sencilla de añadir un disco duro externo por USB y asociarlo a una carpeta, primero como siempre actualizar:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

Antes de empezar tenemos que tener en cuenta que hay unidades USB con alimentación externa y otros sin ella, con alimentación externa no habrá problemas pero debido a las limitaciones de la RaspberryPi en términos de alimentación es posible que no pueda arrancar un disco que requiera alimentación por USB, así que lo primero será encontrar las unidades que tenemos conectadas.

sudo blkid

Si no vemos nuestra unidad y no tiene alimentación externa podemos probar a hacer los siguientes pasos:

Archivos sin fuente de alimentación externa

Editamos el fichero config.txt

sudo nano /boot/config.txt

Agregamos esta línea al final del fichero

max_usb_current=1

Reiniciamos

sudo reboot

Ahora volveríamos a probar el comando blkid y ver si aparece nuestra unidad, de no ser así podemos probar lo siguiente.

Instalamos wringPi desde aquí y comprobamos si tenemos el pin GPIO 38 activo que es el que nos permitirá doblar la potencia de la RaspberryPi y alimentar dispositivos externos:

gpio -g read 38

Si devuelve 1 lo tenemos habilitado, si vemos un 0 lo habilitamos con:

gpio -g write 38 1

En caso de no funcionar hay que usar una fuente de alimentación de más calidad, para verificar si el problema es la fuente de alimentación al arrancar la RaspberryPi tenemos que fijarnos en la cantidad de iconos de frambuesa que veamos, cuantos más mejor y sobre todo fijarnos de no ver ningún símbolo de un rayo lo que querría decir que no hay potencia suficiente.

Configurando unidad externa

Cuando veamos nuestra unidad usamos el siguiente comando:

sudo lsblk -o UUID,NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL,MODEL

Veremos un listado de unidades conectadas, una buena pista suele ser mirar la columna LABEL y MODEL también por el tamaño podemos averiguar cual es nuestra unidad.

La columna FSTYPE muestra el tipo del sistema de archivos, hay que tener instalado el driver adecuado para que se pueda leer, por ejemplo para exFAT:

sudo apt install exfat-fuse

O para NTFS (poder leer y escribir):

sudo apt install ntfs-3g

Tenemos que crear la carpeta en la que queramos mapear la unidad de red, por ejemplo:

mkdir /media/usb/acme

Ahora localizamos el valor UUID (la primera columna) lo guardamos y editamos el fichero /etc/fstab, incluyendo esta línea:

UUID=12345678ABCDEFGH /media/usb/acme ntfs-3g default,uid=pi,gid=pi,nofail 0 0

Donde UUID es el valor que teníamos guardado, luego tenemos la ruta a la carpeta que hemos creado /media/usb/acme, ntfs-3g es el driver necesario para leer la unidad, en este caso ntfs-3g porque es una unidad con sistema de archivos ntfs, uid es el nombre de usuario que se usará para acceder a la unidad (en este ejemplo el usuario pi) y gid es el nombre del grupo con permiso para acceder (en este caso el grupo pi), nofail sirve para indicar que si la unidad no se puede montar en el arranque no se produzca un error.

En este caso hemos usado el usuario pi que viene por defecto en la instalación, si quisiéramos usar otro haríamos:

sudo adduser acme

Igual puede interesar crear un usuario solo para validarse por red pero no para poder iniciar sesión como un usuario normal, algo parecido a una cuenta de servicio.

sudo adduser -shell /bin/false --no-create-home acme

Ahora solo nos queda montar la unidad USB con:

sudo mount -a

Si accedemos a la carpeta que hemos creado veremos el contenido del disco, también podemos compartirla por unidad de red siguiendo los detalles de este post.

Referencias

https://www.htpcguides.com/power-2-5-hard-drive-with-raspberry-pi-b/
https://www.raspberrypi.org/documentation/configuration/external-storage.md
https://www.raspberrypi.org/forums/viewtopic.php?t=238095

USB externo en Raspberry Pi

Reflexiones Fluent Api (I): Introducción

Reconozco que utilizar una librería con Fluent Api puede ser cómodo o según como se haga bastante incómodo, como quería dar una nueva cara a una librería que tengo pensé en hacerla con estilo Fluent Api , podía ser interesante de cara a la librería y también a su facilidad de uso.

De la creación de esa librería y de algunas otras llegué a estas conclusiones que indico aquí (sobre todo para que no se me olviden la próxima vez).

Definición

La definición de Fluent Api está clara en la Wikipedia, esto no quita que haya que tener en cuenta algunos elementos o características propias del lenguaje que se vayan a usar, también indicar que se hace una distinción entre Fluent Api y method chaining, en este caso voy a usar las dos juntas indistintamente pero es importante tener esa distinción en cuenta.

También hay que tener en cuenta que el objeto con el que se está trabajando en Fluent Api es interno a la librería y puede estar compuesto a su vez de otros objetos, la idea de Fluent Api es construir ese objeto interno de una forma correcta para su uso.

Nomeclatura

Es importante tener claro como se van a prefijar los elementos y ceñirse a esta nomenclatura, muchas librerías usan los suyos propios indico aquí un estilo de ejemplo que es el que usaré:

  • AddXXX: Cuando queremos añadir un nuevo elemento al objeto interno, por ejemplo el objeto interno tiene una lista y queremos añadir un nuevo elemento a esa lista:
  • WithXXX: Para añadir una propiedad o establecer un valor al objeto con el que estamos trabajando, puede ser el interno o un objeto dependiente de este.
  • IsXXX / HasXXX: Los dos están orientado a valores booleanos, es verdad que podríamos usar también WithXXX pero muchas veces a los valores booleanos se les da un tratamiento especial para remarcarlos. Si buscásemos una diferencia entre IsXXX y HasXXX podríamos hacer lo siguiente:
    • IsXXX: Para establecer un valor booleano en una propiedad, por ejemplo IsDeveloper, IsCEO.
    • HasXXX: Cuando necesitamos hacer una verificación para obtener esa información o la sacamos de una colección, por ejemplo HasChilds, HasDriverLicense.
  • UseXXX: UseXXX se usa generalmente cuando queremos definir un comportamiento, por ejemplo internamente tenemos varias formas de crear el objeto o de guardarlo y queremos usar una en concreto, también es muy útil para establecer configuraciones.
  • AndXXX: Cuando vamos a concatenar dos propiedades que se configuran por separado pero deben de ir juntas ya que dependen entre sí.

Estilo

En este sentido también hay algunas sugerencias, por ejemplo si la línea de código es bastante larga puede ser interesante separarla en varias líneas, por ejemplo:

var configuration = Builder.Create()
                       .AddElement()
                       .WithProperty()
                       .AndAnotherOne().Save() 

El último método que finaliza se puede colocar también en otra línea aparte o en la misma, lo que interesa remarcar principalmente es cada opción de creación en una línea separada, en este punto puede haber algún problema con los editores de código de cara al formateo y la tabulación pero con un poco de práctica a la hora de generar las líneas o cambiando las opciones de formateo de código es suficiente.

Con los parámetros sucede algo parecido, es posible que los métodos tengan muchos parámetros en ese caso un nivel de indentación adicional ayuda bastante, como se ha comentado dependiendo del editor de código no siempre el formateo de código resulta cómodo así que puede haber variaciones:

var configuration = Builder.Create()
                       .AddElement(
                            text: "Hello")
                       .WithProperty(
                            description: "big",
                            color: "green")
                       .AndAnotherOne().Save() 

Por norma general se intenta usar métodos en todos los casos aunque es verdad que a veces pueda resultar más legible usar una propiedad, realmente es raro tener métodos que no usen parámetros así que por homogeneidad es mejor usar siempre métodos.

No debería pero estas configuraciones pueden llegar a ser largas por lo que incluirlas dentro de un método de configuración en una clase de inicio de la aplicación suele ser la opción más cómoda que es básicamente el comportamiento de los métodos de creación de .Net Core.

Estructura de Fluent Api

Primero recordar lo que dice la propia definición de la Wikipedia sobre la creación de las interfaces:

  • El método de una interfaz siempre devuelve como parámetro la propia interfaz.
  • Siempre hay un método sin contexto final que termina el proceso de creación (Save(), Build(),…)
  • Se trabaja siempre sobre el mismo contexto (u objeto interno) que se construirá.

Voy a usar este ejemplo demostrativo, no pretende ser un ejemplo real simplemente está hecho para poder explicar varios conceptos en los siguientes capítulos.

Diagrama general

Hay dos interfaces, una se encarga de la gestión de usuarios y la otra de la gestión de la conexión, ambas están conectadas a través de una interfaz que representa la configuración de una base de datos.

Reflexiones Fluent Api (I): Introducción