lunes, 26 de diciembre de 2022

Creación de un repositorio Helm

HELM 

Helm es una muy buena herramienta para empaquetar, compartir y desplegar objetos en Kubenetes.

En esta entrada no nos vamos a detener en explicar o detallar lo que es Helm, ni siquiera en cómo instalarlo. Hay suficiente documentación en internet que explica esos extremos. Suponemos, por tanto, que ya está instalado y operativo.

Lo que vamos a hacer aquí es mostrar cómo crear un repositorio propio, en el que podremos ubicar tantos charts como queramos, para nuestro uso y disfrute... y como decíamos arriba, para compartirlos con la comunidad.

El chart que compartiremos servirá para desplegar un servicio Springboot estandar. En nuestro caso, la imagen que invocaremos será una propia tipo "hola mundo", creada a partir de un simple código Springboot, con un endpoint /hello que muestra un mensaje de saludo y el nombre del pod donde corre.


El repositorio lo subiremos a nuestra cuenta de github, y lo publicaremos mediante github pages.

PREPARACIÓN

Bien, comenzaremos por crear el repositorio en github, con nombre "helm-charts". Una vez creado, lo clonaremos a un directorio de trabajo local

    git clone https://github.com/gincol/helm-charts.git
    cd helm-charts

Crearemos una carpeta "charts" donde ubicaremos cualquiera de los charts que deseemos crear

    mkdir charts
    cd charts

Generamos a continuación la estructura de nuestro chart, como digo, destinado a servirnos aplicaciones Springboot

    helm create springboot

Con este comando se nos ha generado la siguiente estructura:


Subimos lo creado hasta el momento

    git add .
    git commit -m "commit inicial"
    git push origin main

Para hacerlo visible vía github pages crearemos una rama "gh-pages"

    git checkout -b gh-pages
    git push origin gh-pages

Vamos a ver si tenemos habilitado github pages en nuestro repo... y efectivamente lo está en la siguiente ruta https://gincol.github.io/helm-charts/, según la configuración siguiente:



Para que sea accesible, en la raíz del proyecto hemos de tener un fichero README, que será el que se muestre:



Para construir y subir todo lo necesario para que nuestro repo sea usable usaremos "chart-releaser". En nuestro caso, que usamos mac, lo instalaremos mediante el gestor brew (https://github.com/helm/chart-releaser)

    brew tap helm/tap
    brew install chart-releaser

    cr help

Para usar de forma cómoda este comando, crearemos el fichero "~/.cr/cr.yaml" con los siguientes datos

    owner: XXXXX    #nuestro id de github
    git-repo: helm-charts
    package-path: .deploy
    token: XXXXXXXXXX #el token generado en "Settings/Developer settings/Personal access tokens" 
    git-base-url: https://api.github.com/
    git-upload-url: https://uploads.github.com/

El token:



Antes de nada, los templates que se generan con el comando "helm create...." están preparados para desplegar un nginx. Por ello haremos algunos cambios en el fichero "templates/deployment.yaml":



Ahora si, empaquetamos (es conveniente añadir la carpeta .deploy en el .gitignore) y subimos

    helm package charts/springboot --destination .deploy
    cr upload

Creamos el index.yaml (donde se describen los charts configurados)

    cr index -i ./index.yaml

La estructura final será:


Finalmente subimos todos los cambios

    git add .
    git commit -m "release 0.1.0"
    git push origin gh-pages

Podemos ver la release en github



PRUEBA

El test lo haremos en un kubernetes ligero, k3s (https://k3s.io/), instalado sobre una MV RHEL9 en un mac vía VirtualBox.

Una vez arrancada la MV y el K3s (por defecto arranca el servicio al arrancar la MV) creamos un directorio de trabajo, y configuramos el values.yaml, en nuestro caso (marco en negrita lo más relevante)


[user@localhost helm-charts]$ cat values.yaml
# Default values for springboot.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
  repository: gines/springboot-normal
  pullPolicy: Always
  # Overrides the image tag whose default is the chart appVersion.
  tag: "1.0.0"

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  # Specifies whether a service account should be created
  create: false
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

podAnnotations: {}

podSecurityContext: {}
  # fsGroup: 2000

securityContext: {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

probe:
  livenessPath: "/actuator/health/liveness"
  readinessPath: "/actuator/health/readiness"

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: traefik
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: rhel.springboot.local
      paths:
        - path: /
          pathType: Prefix
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}


Instalamos el repo
    helm repo add gincol-charts https://gincol.github.io/helm-charts/



e instalamos el paquete
    helm install -f values.yaml springboot-normal --namespace dev gincol-charts/springboot


Accedemos al servicio vía curl

    curl http://rhel.springboot.local/hello
    springboot normal - hello from springboot-normal-6c48557547-5z2hg

Añadimos una réplica modificando el atributo replicaCount del values.yaml, "replicaCount: 2" y hacemos upgrade del servicio desplegado

    helm upgrade springboot-normal gincol-charts/springboot -f values.yaml -n dev

Así nos queda:




Probamos vía navegador la respuesta de ambos pods:



Y eso es todo. Ahora se podrían añadir tantos charts (para node, python, angular...) como nos sea necesario. A partir de ahí, creando el values adecuado podríamos desplegar de una forma sencilla y productiva las aplicaciones que deseemos.

Se podría crear un values por entorno (dev, qa, prod, ...) en una carpeta separada. El comando de despliegue sólo debería utilizar el values de la carpeta que tocase, etc...


domingo, 18 de diciembre de 2022

Cambio de paradigma

Los cambios de paradigma reales han sido pocos a lo largo de la historia, cuesta llegar a ellos y cuesta conseguirlos. Significan cambios culturales, y ya sabéis lo complicado que es asumir nuevos conceptos, nuevas reglas, nuevas formas de pensamiento, nuevas formas de acción, en definitiva nueva cultura (véase T.S Kuhn, "La estructura de las revoluciones científicas).

Hay cambios "mayúsculos", en la medida en que lo nuevo no admite lo viejo, es otra cosa (sistema heliocéntrico copernicano).

Los hay "ligeros", en la medida en que lo viejo se convierte en un caso particular de lo nuevo (física cuántica, relatividad...).

En el caso del Cloud, ¿representa un cambio de paradigma? y en caso afirmativo, ¿de qué tipo?

...

A mi entender si, se trata de un cambio de paradigma, si, y de los "mayúsculos". Representa un antes y un después. Lo viejo es incompatible con lo nuevo, no es un caso particular, no cabe. 

Es dificil encontrar un área en las tecnologías de la información en que este cambio no haya removido sus estructutras, sus modos de pensar y hacer, sus presupuestos, sus metodologías, sus usos y sus costumbres.

El cambio lo inunda todo. Al margen de remover viejas estructuras, ha generado, a la vez, nuevas. Pensemos en todos los XxxOps que han surgido al calor de este nuevo paradigma.

Todos estos XxxOps se presentan a sí mismos como "cultura". Cultura DevOps, cultura GitOps, cultura FinOps..., pero en realidad son cambios culturales menores derivados del cambio cultural padre del que nacen y del que se nutren.

...

De la misma forma que no cabe vuelta a un modelo geocéntrico, no hay vuelta atrás a los modelos previos a la aparición del cloud. Evolucionará, por supuesto, pero no cabe regreso, no es viable.

Más vale que los que vayan retrasados en este cambio lo hagan cuanto antes, más vale que comencemos a pensar en modo "cloud first" para sacar todo el partido posible a las reglas de juego del nuevo modo de pensar tecnológico que representa la aparición del cloud.

Evitemos que las nuevas disciplinas "cloud" y sus profesionales asociados caigan en formas de pensar "antiguas". Evitemos que se generen nuevos "silos".

lunes, 13 de abril de 2020

W10 - Accediendo a la docker-machine (¿?)

Según nos habían dicho, en W10 docker era nativo, a la manera de linux.

Pero no, al final la docker-machine de toda la vida en w7 no se llama docker-machine en W10, pero no deja de ser una máquina virtual.

En W10 usa Hyper-V, esa es la diferencia. En W7 se usaba VirtualBox como driver por defecto cuando creábamos dicha docker-machine. En W10 parece que el hipervisor por defecto (y único diría yo) es Hyper-V.

Aquí tenemos nuestro docker "nativo":


Antes, para acceder a la máquina virtual docker simplemente hacíamos
docker-machine ssh nombre (o bien vía VirtualBox)
 En la actual no parece tan sencillo. Veamos un posible procedimiento (puede haber otros)

Lancemos los siguientes comandos:
docker run --privileged -it -v /var/run/docker.sock:/var/run/docker.sock docker
Aquí ya nos encontramos en un contenedor docker, con root (--privileged), con docker instalado ( con solo ver el nombre de la imagen....). No obstante lo dicho, funciona igualmente sin elevación de privilegios (sin el --privileged). Si queremos obtener datos internos podemos lanzar algunos comandos dentro:



Por consiguiente podemos levantar de nuevo un contenedor (dentro del contenedor, la locura, vaya!!!) a partir del siguiente comando:
docker run --net=host --ipc=host --uts=host --pid=host -it --security-opt=seccomp=unconfined --privileged --rm -v /:/host alpine /bin/sh
Estamos incumpliendo varias normas de seguridad, como usar la red "host", que nos da acceso total  a los recursos de la máquina, pero recordad que estamos en un contenedor.

Ahora si que estamos en la máquina virtual visible desde Hyper-V. Esta es nuestra docker-machine oculta tras el supuesto docker nativo de W10.



Y diréis, ¿y esto qué me aporta? Bueno, básicamente información, conocimiento, que ya de por sí mola!!!

Pero veamos, pregunta, cuando en, por ejemplo, un docker-compose.yml creamos un volumen con el formato

volumes:
      - oracle19_data:/opt/oracle/oradata

volumes:
  oracle19_data:
    driver: local


Tal como hicimos en la anterior entrada "Oracle 19 - docker", ¿dónde se están persistiendo los datos del contenedor (/opt/oracle/oradata)?

Ha!!!!

Pues eso, se están guardando en la MV docker. ¿Y dónde podemos encontrarlos?
Veamos. Primero, fuera, en una consola cmd o PowerShell nueva veamos ese volumen:
docker volume ls
DRIVER              VOLUME NAME
...
local                     oracle_oracle19_data
En el nombre del volumen aparece un prefijo "oracle" añadido por ser el nombre del directorio donde tengo el docker-compose.yml. Veamos la ruta del volumen dentro de la MV docker.


Claro, si en nuestro W10 quisiesemos encontrar esa ruta veríamos que nos sería imposible. Esa ruta no es de nuestro W10.

Ahora es cuando se enlaza todo, si volvemos al último contenedor levantado más arriba y cambiamos el directorio root a /host
chroot /host
 y hacemos un ls, bingo!!!

 

que es justo lo que hay en el mismo contenedor Oracle levantado. Para acabar de validarlo (para los que no se fien) entremos al contenedor Oracle
docker ps
CONTAINER ID      IMAGE   ......
c86159e64799        oracle/database:19.3.0-ee     .......
y hagamos un ls el directorio persistido según la configuración del docker-compose.yml:



Por tanto, de alguna forma es interesante/importante poder verificar o conocer estos detalles, para un conocimiento y control más profundo de docker sobre Windows.



Oracle 19 - docker

Os dejo la forma de crear una BDD Oracle 19.3.0 en local con docker.
Lo primero es descargarse de github todos los ficheros necesarios:


A continuación nos descargamos el zip con el binario de la versión que queramos, en nuestro caso la 19.3.0 Una vez descargado (alrededor de 3Gb) movemos el zip a la carpeta dockerfiles/versión (en nuestro caso a dockerfiles/19.3.0).

Nos movemos a la carpeta "dockerfiles" y lanzamos (en W10 habiendo habilitado WSL hacedlo con un debian o ubuntu - la carpeta /mnt mapea vuestro W10, pudiendo acceder a la unidad C:)

  • ./buildDockerImage.sh -e -i -v 19.3.0

Es posible que debamos pasarle el dos2unix de forma recursiva desde la carpeta actual.
Esperamos a que acabe, tras lo cual tendremos la imagen generada "oracle/database:19.3.0-ee".
Por último, creamos un "docker-compose.yml" para facilitar el run de la imagen


version: '3.5'
services:
  oracle19:
    container_name: oracle19
    image: oracle/database:19.3.0-ee
    ports:
      - "1521:1521"
      - "5500:5500"
    environment:
      ORACLE_SID: docker19sid
      ORACLE_PDB: docker19pdb
      ORACLE_PWD: dockerpass
      ORACLE_CHARACTERSET: AL32UTF8
    volumes:
      - oracle19_data:/opt/oracle/oradata

volumes:
  oracle19_data:
    driver: local


Ya sólo quedará lanzar el comando "docker-compose up -d" desde la ubicación del yaml anterior. El primer lanzamiento tarda un poco, paciencia. Si véis que le cuesta mucho o véis en el log del contenedor warnings por escasez de memoria, aumentar a 6 Gb la memoria asignada a Docker (en W10 acceded al dasboard, opción Resources)

Como consejo, si creáis un Tablespace, hacedlo indicando la ruta del datafile explicitamente, y dentro del directorio persistido vía volumen. Si no indicáis ruta lo crea en una por defecto que cae fuera de dicho volumen (¿?). En el siguiente run de la imagen obtendréis un catastrófico error al no encontrar dicho datafile no pudiendo arrancar la instancia.

Por ejemplo:

  • CREATE TABLESPACE T_MYTABLESPACE DATAFILE '/opt/oracle/oradata/datafiles/T_MYTABLESPACE.dbf' SIZE 1000M AUTOEXTEND ON ONLINE;
  • CREATE USER MYUSER IDENTIFIED BY "MYUSER" DEFAULT TABLESPACE T_MYTABLESPACE TEMPORARY TABLESPACE temp;
  • GRANT ALL PRIVILEGES TO "MYUSER";

Y listos!!!

miércoles, 5 de septiembre de 2018

Laboratorio - Integración Contínua (VIII)

Octava entrada en la creación de nuestro laboratorio CI. Recuerdo los pasos:
  1. Instalación de Ubuntu en VMWare Player en Windows 7
  2. Instalación de Jenkins en Ubuntu
  3. Instalación GitLab en Ubuntu
  4. Instalación de Docker Minikube en Ubuntu
  5. Desarrollo de un microservicio en Eclipse Windows (1) y (2)
    1. Angular 6 frontend
    2. SpringBoot 2.x backend
    3. Mongo Replicaset (3 replicas)
  6. Despliegue CI en Minikube utilizando la infraestructura configurada
    1. Pipeline - Jenkinsfile
    2. GitLab webhook
    3. Dockerfile
    4. Deploy y Service yaml
    5. Secret
    6. Ingress

Nos ocupamos en esta entrada de lo que hemos convenido en llamar entorno "dev", aquel entorno local con kubernetes.

Aquí añadimos configuración específica para desplegar nuestra aplicación en minikube. Además, detallaremos la configuración que nos permitirá realizar y completar nuestro laboratorio de Integración continua (Webhook GitLab y Pipeline Jenkins). 

Comenzamos primero con el detalle de nuevos "yaml" que han de acompañar a nuestra aplicación. En la segunda parte de esta entrada especificaremos la configuración de CI.

Pero antes de nada, hemos de realizar un par de configuraciones en minikube


Minikube
Antes de nada hemos de arrancar Minikube. Lo más conveniente es arrancarlo con el usuario con el que se lancen más adelante los despliegues. 

En nuestro caso vamos a usar Jenkins como herramienta de despliegues. En el proceso de instalación de este producto se crea el usuario jenkins. Este es el usuario que debemos utilizar para el arranque de Minikube.

Para poder usar dicho usuario le hemos de asignar un password (por defecto no lo tiene):
    sudo passwd jenkins

    cuando nos pregunte el password le ponemos el que deseemos

Lo incluimos además en los grupos "sudoers" (para poder ejecutar comandos con "sudo") y "docker"
    sudo usermod -a -G sudoers,docker jenkins

Ahora ya podemos acceder
    su jenkins

    proporcionamos el password configurado más arriba.


Registro privado
Para poder trabajar con imágenes en un entorno CI es conveniente crearnos un registro privado. Lo utilizaremos como repositorio de imágenes de forma que por cada aplicación nueva dispondremos de un lugar local y centralizado donde depositarlas y desde el cual referenciarlas.

Minikube lleva un addon de registro, pero no lo vamos a utilizar. En alguna otra entrada experimentaremos con él. De momento utilizaremos una imagen pública que hace las veces de registro. Vamos a ello.

En línea de comandos accedemos con usuario jenkins tal como se indicó más arriba. Arrancamos minikube
    minikube start

Una vez arrancado cargamos las variables de minikube
    eval $(minikube docker-env)

Descargamos y arrancamos la imagen de registro:
    docker run -d -p 5000:5000 --restart=always --name registry-srv -v /data/docker-registry:/var/lib/registry registry:2

Esta imagen nos proporciona una api para acceder a información de imágenes, tags, etc.... La url de acceso será "localhost:5000". Esa será la url que usaremos para subir o bajar imágenes de nuestro registro privado. Antes de probar la api habremos de añadir la entrada siguiente al "/etc/hosts" de ubuntu

    192.168.99.100  localhost
    la ip es la de minikube (se puede consultar con "minikube ip").

    Ahora podemos consultar el catálogo de imágenes:
    curl -X GET http://localhost:5000/v2/_catalog

    al no haber subido ninguna imagen nos devolverá
    {"repositories":[]}

    Cuando subamos alguna imagen podremos ver también los tags (versiones) de la misma:
    curl http://localhost:5000/v2/NOMBRE_IMAGEN/tags/list

Una vez tenemos el registro privado operativo hemos de parar minikube para arrancarlo en forma "insecure". Nos evitaremos de esta forma el acceso con claves, certificados, etc... 

Paramos con "minikube stop". Y ahora volvemos a arrancar 
    minikube start --insecure-registry localhost:5000 --memory 4096 (la reserva de memoria es opcional)

Siempre habremos de arrancar minikube con el flag "--insecure-registry localhost:5000". 

Minikube addons
Al arrancar por primera vez minikube con usuario jenkins deberemos activar uno de los addons necesarios para proveer de una url específica a nuestra aplicación u otras que queramos desplegar en este entorno.

Antes revisamos los addons activos
    minikube addons list

Podemos habilitar el addon heapster (ver entrada) aunque para esta entrada no es imprescindible.

El addon que si necesitaremos es "ingress", el cual por defecto no está activo, por lo que hemos de activarlo
    minikube addons enable ingress

Este addon nos proporcionará un balanceador a partir de un nombre dns local. Más adelante veremos cómo usarlo.


secret
En este laboratorio, a modo de ejemplo "avanzado", vamos a proveer de acceso vía HTTPS a nuestra aplicación. Para ello hemos de crear un secret. 

Primero creamos los certificados correspondientes (clave pública y privada). Al dominio local lo vamos a llamar "gincol.blog.com" (podéis ponerle el que mejor os vaya).

Creamos un directorio de trabajo (cloud/secrets) en la home del usuario jenkins "/var/lib/jenkins/cloud/secrets" y nos posicionamos en él.

Creamos las claves pública y privada con openssl para el dominio comentado (gincol.blog.com)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout gincol.blog.com.key -out gincol.blog.com.cert -subj "/CN=gincol.blog.com/O=gincol.blog.com"


Desde el mismo directorio de trabajo, creamos ahora el secret a partir de los certificados creados
    kubectl create secret tls gincol.blog.com-secret --key gincol.blog.com.key --cert gincol.blog.com.cert

Validamos
    kubectl get secrets



Una vez tenemos el nombre de nuestro dominio privado, creados los certificados y secret, hemos de dar de alta este dominio en nuestro "dns privado", es decir, en el "/etc/host" de ubuntu. La ip asociada será la de minikube.

Revisamos dicha ip
    minikube ip

    habitualmente se asigna la "192.168.99.100"

añadimos pues la siguiente entrada en dicho "/etc/hosts"
    192.168.99.100  gincol.blog.com





Configuración yaml

Este es el detalle le los múltiples ficheros yaml que deberemos añadir a nuestra aplicación. Todos ellos los incluiremos en la carpeta cloud del proyecto ci-root


ingress
Como ya comentamos, este addon nos proporciona un balanceador asociado a un dominio y para un servicio dado. En nuestro caso, para la aplicación de ejemplo, esta es su configuración (Ingress.yaml):

    apiVersion: extensions/v1beta1
    kind: Ingress
    metadata:
      name: ci
      annotations:
        kubernetes.io/ingress.class: nginx
    spec:
      tls:
        - hosts:
          - gincol.blog.com
          secretName: gincol.blog.com-secret
      rules:
      - host: gincol.blog.com
        http:
          paths:
          - path: /ci
            backend:
              serviceName: ci-app
              servicePort: 8080

Como se puede ver se referencia al secret creado en el anterior apartado (gincol.blog.com-secret).
Se proporciona un path "/ci", además de un puerto, el "8080". El nombre del servicio será "ci-app". Este nombre lo veremos más adelante en la configuración del service.

El nombre del dominio es, como ya hemos mencionado antes, "gincol.blog.com".


volume
Pra proveer de persistenca a la bdd mongo hemos de crear un volumen. Para ello serán necesarios dos ficheros de configuración, el "PersistentVolume.yaml" y el "PersistentVolumeClaim.yaml".

El primero es el que define un volumen "general". El segundo es el "reclama" una parte de espacio al anterior. Este último será el que referenciemos en el deployment de mongo.

La configuración del "PersistentVolume.yaml" es la siguiente:

    kind: PersistentVolume
    apiVersion: v1
    metadata:
      name: pv-blog-1
      labels:
        type: local
    spec:
      storageClassName: manual
      capacity:
        storage: 10Gi
      accessModes:
        - ReadWriteOnce
      hostPath:
        path: /data/pv-blog-1/

Reservamos 10 Gb de espacio local. El path asignado será "/data/pv-blog-1". Este path lo podremos encontrar dentro de minikube.

La configuración del "PersistentVolumeClaim.yaml" es esta:

    kind: PersistentVolumeClaim
    apiVersion: v1
    metadata:
      name: blog-claim
    spec:
      storageClassName: manual
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi

Kubernetes comprobará el atributo "storageClassName", y buscará un "Persistent Volume" del mismo tipo. Una vez encontrado asignará la memoria pedida en el "PersistentVolumeClaim.yaml", 1 Gb en nuestro caso, del total de memoria configurada, 10 Gb.

El nombre que habremos de referenciar en el deployment de mongo será "blog-claim" para hacer uso de este espacio.


configmap
Un ConfigMap es utilizado para contener pares de tipo "clave: valor" que habitualmente serán referenciados desde los ficheros yaml de deployment de las aplicaciones.

Este es su contenido:

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: ci-config
      namespace: default

    data:
      spring.profiles.active: dev

En el apartado de "data" es donde se pueden poner tantos pares "clave: valor" como se quieran. Nosotros sólo necesitamos indicar el profile activo para este entorno, "dev". La variable podría haber sido cualquiera, hemos puesto "spring.profiles.active" pero podría haber sido "mi.profile", por ejemplo.


service y deployment Aplicación
El deployment y service son los descriptores principales de nuestra aplicación. En el caso de la aplicación web tendrá este contenido (APP.yaml):

    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: ci-app
      labels:
        name: ci-app
    spec:
        replicas: 2
        template:
          metadata:
            labels:
              app: ci-app
              tier: backend
          spec:
            containers:
              - name: ci-app
                image: localhost:5000/ci-app
                env:
                  - name: SPRING_PROFILES_ACTIVE
                    valueFrom:
                      configMapKeyRef:
                        name: ci-config
                        key: spring.profiles.active
                ports:
                  - containerPort: 8080
                readinessProbe:
                  tcpSocket:
                    port: 8080
                  periodSeconds: 30
                  initialDelaySeconds: 120
                  timeoutSeconds: 5  
                livenessProbe:
                  httpGet:
                    path: /ci/liveness
                    port: 8080
                  periodSeconds: 30
                  initialDelaySeconds: 120
                  timeoutSeconds: 5
            
    ---
    kind: Service
    apiVersion: v1
    metadata:
      name: ci-app
      labels:
        name: ci-app
        tier: backend
    spec:
      type: NodePort
      ports:
      - port: 8080
        protocol: TCP
      selector:
        app: ci-app
        tier: backend
  

Se han configurado dos "Probe", uno de arranque, el "readinessProbe" y otro de actividad, el "livenessProbe". Hasta que el readiness no responda OK Kubernetes no enviará peticiones al servicio. 

La variable del configmap es recogida a partir de la configuración "env". Ahí se mapea la variable que nuestra aplicación necesita "SPRING_PROFILES_ACTIVE" con el valor de la variable del configMap "spring.profiles.active", la cual como ya hemos visto vale "dev".

La imagen es recogida desde el registro, a partir de "localhost:5000". Se añade además el nombre de la imagen, que la encontraremos definida en el Jenkinsfile que veremos más abajo.

El servicio expone el puerto 8080 del contenedor. El name "ci-app" es el que se asocia al "serviceName" del ingress anteriormente detallado.

Se han configurado dos replicas, por lo tanto el balanceador enviará peticiones tanto a una como a otra. Habitualmente se seguirá un modelo "round robin", una petición a un pod, la siguiente al otro pod, etc...


service y deployment Mongo
En cuanto mongo, se ha configurado mediante el service y en lugar de deployment se ha usado un StatefulSet.

Esto es así ya que como veremos cuando detallemos el Jenkinsfile, necesitamos un "nombre" constante. El deployment kubernetes genera un nombre con una parte fija y otra variable. Un StatefulSet genera un nombre constante, fácilmente referenciable.

Esta es su configuración:

    apiVersion: v1
    kind: Service
    metadata:
      name: ci-db
      labels:
        name: mongo
    spec:
      ports:
      - port: 27017
        targetPort: 27017
      clusterIP: None
      selector:
        role: mongo
    ---
    apiVersion: apps/v1beta1
    kind: StatefulSet
    metadata:
      name:  ci-ss
    spec:
      serviceName: ci-db
      replicas: 3
      updateStrategy:
        type: RollingUpdate
      template:
        metadata:
          labels:
            role: mongo
            environment: dev
            replicaset: CiRepSet
        spec:
          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
              - weight: 100
                podAffinityTerm:
                  labelSelector:
                    matchExpressions:
                    - key: replicaset
                      operator: In
                       values:
                      - CiRepSet
                  topologyKey: kubernetes.io/hostname
          terminationGracePeriodSeconds: 10
          containers:
            - name: ci-db
              image: localhost:5000/ci-db
              command:
                - "numactl"
                - "--interleave=all"
                - "mongod"
                - "--wiredTigerCacheSizeGB"
                - "0.1"
                - "--bind_ip"
                - "0.0.0.0"
                - "--replSet"
                - "CiRepSet"
              resources:
                requests:
                  cpu: 0.2
                  memory: 200Mi
              ports:
                - containerPort: 27017
              volumeMounts:
                - name: blog-claim
                  mountPath: /data/db
      volumeClaimTemplates:
      - metadata:
          name: blog-claim
          annotations:
            volume.beta.kubernetes.io/storage-class: "standard"
        spec:
          accessModes: [ "ReadWriteOnce" ]
          resources:
            requests:
              storage: 1Gi


Como veis, contiene mucha información. Detallamos sólo una parte. 

La asociación con la persistencia se hace mediante el nombre del "claim" configurado anteriormente, a saber, "blog-claim"

Se nombra el replicaset "ci-ss", que deberemos referenciar en el Jenkinsfile. El nombre "CiRepSet" también será referenciado desde la url de conexión de la aplicación hacia mongo.

Se indican 3 réplicas, por lo que tendremos un mongo con un nodo "PRIMARY" y dos nodos "SECONDARY". Cualquier modificación que hagamos sobre uno se hará sobre otro. Si se parase uno, por ejemplo el PRIMARY, Kubernetes arrancará otro nodo, y uno de los SECONDARY pasará a ser PRYMARY, etc...
 
Otros ficheros
Dockerfile
El Dockerfile lo encontramos en la carpeta docker de la raíz del proyecto ci-root. Tiene este contenido:

    FROM java:openjdk-8-jdk-alpine

    # add el jar con el nombre del "finalName" del pom.xml
    ADD ci-backend/target/ci-backend.jar /app.jar

    # se modifica la fecha a la actual
    RUN sh -c 'touch /app.jar'

    # comando a ejecutar

    CMD ["java", "-jar", "/app.jar"]

El nombre del fichero es el mismo que ya vimos en la la entrada 3, es decir, Dockerfile-app.


application-dev.yml 

El fichero de configuración de la aplicación merece comentarlo, aunque sólo sea lo más importante

    spring:
      port: 8080
      servlet:
        context-path: /ci
      ...
      data:
        mongodb:
          host: ci-db
          port: 27017
          database: ci
          uri: mongodb://ci-db:27017/ci?replicaSet=CiRepSet
      autoconfigure:
        exclude: org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration



Se le añade un contexto a la aplicación (/ci) que veremos en la url de acceso. El texto en rojo contiene el nombre del servicio de mongo declarado en el fichero "DB.yaml"
El texto en azul contiene el nombre de la base de datos.
El texto en lila corresponde al nombre del replicaset declarado en el fichero "DB.yaml", y también referenciado en el Jenkinsfile.

Se excluye el uso de mongo embedded con "exclude".



Configuración CI
Pasamos a la configuración que nos permitirá disponer de un entorno CI completo. Para ello abrimos Firefox de Ubuntu y realizamos las configuraciones siguientes: 


GitLab
Accedemos al GitLab del Ubuntu, en mi caso a partir de la url "http://gitlab.blog.com:81". Usamos el grupo "blog" ya creado en la tercera entrada.

Creamos el proyecto "ci-root" en su interior



La url del proyecto dentro de GitLab es "http://gitlab.blog.com:81/userblog/ci-root.git". Nos la apuntamos.

Ya tenemos el proyecto listo para recibir código. No obstante, se deberá hacer una configuración adicional para enlazarlo con Jenkins. En el apartado "Enlace Jenkins-GitLab" lo veremos.


Jenkins
Accedemos a Jenkins (en mi caso "http://localhost:8787") y creamos un job como copia del que ya creamos en la tercera entrada.

Hacemos "New Item", entramos un nombre ("ci-root" en nuestro caso) y en "Copy from" ponemos "HolaMundo-CI" (el que ya creamos en la tercera entrada comentada).



Pulsamos finalmente sobre "OK".



Enlace Jenkins-GitLab
El enlace se hace en las dos direcciones. Le hemos de decir a GitLab (vía webhook) que cuando se haga un push del proyecto, informe a Jenkins de que tiene nuevo código disponible para su despliegue.

Hemos de configurar en Jenkins la ruta del repositorio GitLab de donde extraer el código cuando reciba el aviso de GitLab.

Configuramos primero el pipeline Jenkins creado anteriormente ("ci-root"). 
Primero generamos un nuevo tocken, diferente del que había configurado al crear el job como copia del ejemplo del que partimos. Nos quedamos el nuevo token (en nuestro caso nos ha generado el "151f0d9e05f6dee509c77fe18eb6d508").

En Pilepline Definition, apartado "Repository URL" ponemos la que apuntamos más arriba "http://gitlab.blog.com:81/userblog/ci-root.git". El resto del job lo dejamos tal cual quedó tras su creación.

Anotamos la url del job, la encontramos en el apartado "Build Triggers", en nuestro caso es "http://192.168.153.201:8787/project/ci-root".


En GitLab creamos el webhook, "Settings / Integrations".


Pulsamos el botón "Add webhook". Informamos la url del pipeline de Jenkins y el Token del mismo pipeline.

Ya tenemos el enlace listo. Ahora cada vez que hagamos un push de código, GitLab informará a Jenkins, y este vendrá a recoger el código, lanzanado el contenido del fichero Jenkinsfile, que es el orquestador final del despliegue de la app en Kubernetes.

Veamos dicho Jenkinsfile


Jenkinsfile
Este fichero lo ubicamos en la raíz del proyecto "ci-root". Será leído y ejecutado por el job jenkins descrito más arriba. Su contenido es excesivamente largo para copiarlo aquí, por lo que lo podéis ver al descargar el proyecto del gitlab público donde hemos dejado todo el código.

Como resumen:

Al comienzo, en tools, se referencian las herramientas a usar (maven - mvn53, y java - java8). Estas variables se dieron de alta en Jenkins (ver segunda entrada).

En el stage "Inicializacion" se logan los path de dichas tools como validación de su existencia. También se cargan las variables de minikube para poder desplegar los diferentes elementos de forma correcta.

En el segundo stage, "Construccion" se lanza maven para la construcción del artefacto.

En el stage "Push de las ....", se crean las imágenes y se suben al registro privado

En el stage "Deploy Minikube" se despliegan los diferentes elementos en Minikube. Especial atención merece el apartado de creación del replica set mongo. El comando para tres réplicas es un tanto aparatoso:

sh "kubectl exec ci-ss-0 -c ci-db-container -- mongo --eval 'rs.initiate({_id: \"CiRepSet\", version: 1, members: [ {_id: 0, host: \"ci-ss-0.ci-db.default.svc.cluster.local:27017\"}, {_id: 1, host: \"ci-ss-1.ci-db.default.svc.cluster.local:27017\"}, {_id: 2, host: \"ci-ss-2.ci-db.default.svc.cluster.local:27017\"} ]});'"

Como se puede ver, aquí es donde se aprecia la importancia de usar un StatefulSet en lugar de un Deployment. Los nombres que Kubernetes genera al desplegar Mongo será "ci-ss-0", "ci-ss-1" y "ci-ss-2". Este nombre viene dado por el nombre definido en su respectivo yaml (visto más arriba).

En el apartado "Borrado de imagenes" se hace limpieza de las imágenes innecesarias.

En el último apartado se loga el resultado final (OK, KO, etc...) de la ejecución del job.



Prueba 
Bueno, después de tantas entradas y de tantas configuraciones ya estamos en disposición de probar nuestro entorno CI.

Arrancamos nuestra consola git bash de windows y nos posicionamos en la raíz del proyecto ci-root. Debería tener esta composición:



Creamos el repositorio git local.
    git init
    git remote add origin http://USER:PASSWORD@PATH_REPO_GIT

en nuestro caso, este segundo comando queda así
    git remote add origin http://userblog:userblog@gitlab.blog.com:81/userblog/ci-root.git

Añadimos fichero, hacemos commit de los cambio y finalmente push
    git add .
    git commit -m "subida inicial"
    git push origin master

Nos debe aparecer una salida como la siguiente:


Si accedemos a GitLab veremos el proyecto subido:


Si accedemos a Jenkins veremos el lanzamiento del pipeline: 




En los logs del job se puede ver como se han descargado las imágenes base de mongo (mongo:latest) y java (java:openjdk-8-jdk-alpine)

También podemos revisar las imágenes subidas al registro privado
    curl -X GET http://localhost:5000/v2/_catalog

    que nos responde ahora:
    {"repositories":["ci-app","ci-db"]}

    y los tags de cualquiera de ellas
    curl http://localhost:5000/v2/ci-app/tags/list

    {"name":"ci-app","tags":["latest"]}


Podemos revisar los services, deploys, pods, etc..., generados
    kubectl get svc
    kubectl get deploy
    kubectl get pods

Si alguno lo vemos en un estado de error, si no nos deja ver los logs ya que el pod realmente no ha llegado a crearse, podemos ver su "descripción" con
    kubectl describe pod POD_ID

    Al final de la salida podremos ver la causa del error.

Si queremos ver los logs de uno o ambos de los pods de la aplicación podemos hacer
    kubectl logs -f POD_ID   (con -f queda la consola attachada a la salida de log)

Si queremos ver los logs de ambos pods o bien abrimos dos shell, una por cada pod, o bien nos descargamos el script "kubetail" de la url "https://github.com/johanhaleby/kubetail", creamos el script por ejemplo en el fichero "/cloud/utils/kubetail" de la home de usuario jenkins. Accedemos a dicho path y hacemos
    ./kubetail ci-app

No mostrará algo como lo siguiente:




Como vemos, los logs de cada pod aparecerán en un color diferente y veremos que al acceder a la aplicación las peticiones son respondidas indistintamente por uno u otro.

Podemos ver el estado del replicaset de mongo desde línea de comandos
    kubectl exec ci-db-ss-0 -c ci-db-container -- mongo --eval 'rs.status()'

Nos devolverá un json con el estado de las tres réplicas. Una de ellas nos la marcará como "PRIMARY", y las otras dos como "SECONDARY".

Podemos hacer un delete del pod marcado como PRIMARY. Veremos que automáticamente minikube levanta otro pod en su lugar (siempre ha de haber tres réplicas levantadas) y cualquiera de los que han quedado levantados pasarán a ser PRIMARY, etc...

Ahora accedemos a la aplicación con la url "https://gincol.blog.com/ci":



Podemos jugar un poco con la creación, edición, etc...



También podemos acceder a swagger a partir de "https://gincol.blo.com/ci/swagger-ui.html".


Tras el acceso y uso de la aplicación vemos, como decíamos más arriba accesos a ambos pods




Final
Bueno, el camino ha sido duro y largo, pero creo que ha merecido la pena. Si alguien trabaja en entornos cloud, es necesario disponer de un entorno local de prueba, que no será exactamente igual que el real, pero se le aproximará bastante.

Espero que os haya sido de ayuda. El código completo está en nuestro repo público del GitLab.


Anterior