---
title: "Best Practices in the deployment of Openshift/Kubernetes (k8s)"
canonical: "https://onesaitplatform.refined.site/space/DOCT/2220812757/Best%20Practices%20in%20the%20deployment%20of%20Openshift%2FKubernetes%20(k8s)"
format: markdown
---
So as to deploy not only Onesait Platform but any application containerised in Openshift or Kubernetes (k8s) clusters, you will need to follow a number of guidelines or Best Practices, which range from the definition and construction phase of your Docker image, to the phase of creating Kubernetes manifests, "deploying" them and distributing your software as a piece or package with Helm.


> Macro (toc)

> ℹ️ Pre-requisites:
> ℹ️ 
> ℹ️ - Docker locally installed.
> ℹ️ - Helm v3 locally installed.<sup>1</sup>
> ℹ️ - Kubernetes cli (kubectl) or Openshift cli (oc)<sup>1</sup> to be able to communicate and operate your deployments against the cluster.
> ℹ️ 
> ℹ️ <sup>1</sup>If you use Openshift, you can download both the OCP cli and Helm from the link below.

![image](media://08bc6762-2c5b-4130-971b-8e8b58c81c82)

### Building the Docker image


Para desplegar una imagen Docker en Openshift hay que ser especialmente cuidadosos con la seguridad, no arrancando nuestros contenedores con usuario root o intentando modificar en ejecución directorios del filesystem propiedad de root.

- Reducir número de capas de la imagen: Para ello, trataremos de poner en la misma linea aquellas instrucciones que generan capas en Docker: **RUN**, **COPY** y **ADD** . Por ejemplo, en la siguiente imagen se muestra la creación de dos directorios distintos con **mkdir** dentro de la misma instrucción **RUN**:
- En imágenes muy grandes es conveniente comprimir las capas en una única mediante el flag **--squash **al construir la imagen con *docker build*

> ⚠️ Al hacer squash de las capas de una imagen se pierde el reaprovechamiento de capas, es recomendable hacerlo sólo en aquellos casos estrictamente necesario o en imágenes de un elevado tamaño pero que no vayan a cambiar con frecuencia en el tiempo

- Crear un usuario para la ejecución de nuestra aplicación dentro del contenedor: Evitaremos lanzar nuestro contenedor con usuario root, lo más habitual es que nos de problemas y por seguridad no conviene hacerlo. Para ello nos crearemos un usuario, un grupo y daremos permisos a este usuario en los directorios que tenga permisos o en los que creemos nosotros y le hagamos propietario de los mismos.
- Crear directorios, dar permisos a los mismos: Como consecuencia de lo anterior, daremos a los directorios anteriores permisos para el usuario creado
- Delegar la ejecución del contenedor al usuario creado: Finalmente con la instrucción **USER** seguida del usuario creado (y opcionalmente el grupo) indicaremos que el usuario con el que se ejecuta el contenedor no es root.
- Añadir la meta información necesaria: etiquetas, puertos y volúmenes: De un vistazo al Dockerfile deberíamos poder saber cómo se llama el módulo, que puertos y qué volúmenes expone, para ello haremos uso de las instrucciones **LABEL**, **EXPOSE** y **VOLUME **

### Definición de los Manifest de Kubernetes

Una vez que hemos generado nuestra imagen Docker conforme a las pautas anteriores, el siguiente paso es poder desplegarla en un cluster de Kubernetes o en Openshift. 

Para desplegar en Kubernetes/Openshift tendremos que apoyarnos en los ficheros Manifest en formato .yaml. Cada uno de estos ficheros representa un objeto de Kubernetes, <u>los hay de muchos tipos para cometidos muy distintos</u>, pero los habituales y por donde deberemos empezar serán los siguientes:

- **Deployment**: Define como van a ser nuestros pods (imágenes, tags, puertos, volúmenes, nombre, etc…) y el mecanismo de Replica Set. Recordemos que en Kubernetes la unidad mínima de despliegue es el pod, donde un pod está formado por uno o más contenedores en ejecución, partiendo de la misma o distinta imagen, y que comparten red y espacio de almacenamiento. Por otro lado el Replica Set asociado al Deployment mantiene estable el número de pods indicados en el factor de réplica del Manifest del Deployment. En este manifest podremos definir como se va a llamar nuestra aplicación, la imagen de nuestro/s pod/s, número de réplicas de los mismos, puertos, política de reinicio, política de descarga de imágenes (Always o IfNotPresent)

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apimanagerservice
  namespace: onesait-platform
  labels:
    app: apimanagerservice
    group: onesait-platorm
spec:
  replicas: 1
  selector:
    matchLabels:
      app: apimanagerservice
  template:
    metadata:
      labels:
        app: apimanagerservice
    spec:
      containers:
      - name: apimanagerservice
        image: registry.onesaitplatform.com/onesaitplatform/apimanager:latest
        imagePullPolicy: Always
        env:
        - name: HZ_SERVICE_DISCOVERY_STRATEGY
          value: zookeeper
        ports:
        - containerPort: 19000
          name: 19100tcp191002
          protocol: TCP
      dnsPolicy: ClusterFirst
      restartPolicy: Always
```

En el caso de que en el cluster existan quotas y límites de uso de cpu y memoria tendremos que definirlos en nuestro manifest de esta manera:

```yaml
        resources:
          limits:
            cpu: "500m"
            memory: "3Gi"
          requests:
            cpu: "250m"
            memory: "500Mi"
```

Además es conveniente en la sección de metadatos añadir la etiqueta “group“ y el nombre de nuestra aplicación. De esta manera, cuando se despliegue en el mismo namespace tanto la Onesait Platform como la/s aplicación/es que la utilizan podremos filtrar por esta etiqueta.

```yaml
  labels:
    app: apimanagerservice
    group: onesait-platorm
```

![image](media://ff28baf1-d9b7-4a75-ad2d-341eba245f04)

Filtrado por labels en Openshift:

![image](media://471d6f98-2f01-4c92-a6b2-45d6294bb5f0)

- **Service**: Un servicio nos va a permitir exponer nuestro pod/conjunto de pod’s como un servicio de red. Cada pod tiene asignada su propia ip y el servicio realizará el balanceo de peticiones entre los pods pertenecientes al mismo Deployment. El servicio quedará “asociado” con el despliegue mediante los selectores de etiquetas, en el ejemplo buscará el deployment etiquetado con el selector app=apimanagerservice. El puerto a exponer en este caso será el mismo del pod, el 19000. El tipo de servicio, típicamente será de tipo **ClusterIp**, es decir el servicio sólo será alcanzable dentro del cluster.

```yaml
apiVersion: v1
kind: Service
metadata:
  name: apimanagerservice
  namespace: onesait-platform
  labels:
    run: apimanagerservice
    k8s-app: apimanagerservice
spec:
  ports:
  - port: 19000
    protocol: TCP
    targetPort: 19000
  selector:
    app: apimanagerservice
  type: ClusterIp
  sessionAffinity: None
```

- **Persistent Volume Claim**: Un pvc es una petición de almacenamiento sobre un persistent volume, que no es más que un recurso de almacenamiento del cluster (por ejemplo un directorio del sistema operativo, un disco NFS, un recurso de almacenamiento de Azure, etc…) y nos permitirá persistir los datos de nuestros pods.

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: configdb-pvc
  namespace: onesait-platform
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: managed-premium
  resources:
    requests:
      storage: 1Gi
```

Luego para acceder al pvc desde nuestro pod tendremos que indicarlo en el Manifest del Deployment, indicando tanto el nombre del pvc (**claimName**) como el directorio que queremos persistir del pod (**mountPath**) y el **subpath** que es el directorio dentro del persistent volume donde se almacenará

```yaml
        volumeMounts:
        - mountPath: /var/lib/mysql
          name: configdb-data
          subPath: configdb
...
      volumes:
      - name: configdb-data
        persistentVolumeClaim:
          claimName: configdb-pvc          
```

- **Secret**: Los secretos de Kubernetes permitirán almacenar información sensible y accesible desde nuestros pods, como contraseñas y sobre todo claves ssh y certificados. En el ejemplo el secret almacena en data el certificado y la clave en base64, luego será accesible por el pod como un volumen

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: loadbalancersecret
  namespace: onesait-platform
type: kubernetes.io/tls
data:
  tls.crt: LS0tLS1CRUdJTiBDR...
  tls.key: LS0tLS1CRUdJ...
```

En nuestro deployment podremos acceder al certificado a través del secret como si fuese un persistent volume claim:

```yaml
        volumeMounts:
        - mountPath: /tmp
          readOnly: true
          name: ssl-data
...
      volumes:
      - name: ssl-data
        secret:
          secretName: loadbalancersecret
```

- **Ingress**: Para dotar de acceso externo al cluster, es decir, para permitir el tráfico http y https a los servicios internos del cluster está el objeto Ingress. Podemos verlo como un proxy inverso, de hecho es lo que es, ya que uno de los pre requisitos es que el cluster de Openshift/Kubernetes tenga un Ingress Controller instalado previamente, siendo el más usado el de nginx ([https://github.com/kubernetes/ingress-nginx](https://github.com/kubernetes/ingress-nginx)) en el ejemplo podemos ver como mediante nuestro Ingress exponemos el tráfico con terminación tls (https) al servicio interno del cluster **loadbalancerservice**, usa un certificado almacenado en un secret (**loadbalancersecret**) y redirige el tráfico a la ruta raíz “/“

```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: loadbalancer
  namespace: onesait-platform
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/proxy-body-size: "256m"
spec:
  tls:
  - hosts:
    - example.onesaitplatform.com
    secretName: loadbalancersecret
  rules:
  - host: example.onesaitplatform.com
    http:
      paths:
      - backend:
          serviceName: loadbalancerservice
          servicePort: 8443
        path: /
```

### Despliegue en Openshift

Si queremos desplegar los manifest anteriores en Openshift <u>uno a uno</u>, podremos hacerlo desde la propia UI de Openshift de la siguiente manera:

- **Deployment**: Desde la opción de menú *Workloads → Deployment → Create Deployment*

![image](media://2001aa07-adb2-43bf-bbd5-a0bbd7ac85bf)

Y pegaremos nuestro manifest para posteriormente poder crearlo:

![image](media://eb05c4af-d4f5-4a72-b17b-438e10b67155)

Una vez creado el Deployment, automáticamente nos creará el **ReplicaSet** (asegura que el número de pods establecido se mantiene siempre constante) lo podremos ver en la opción *Workloads → ReplicaSet*.

![image](media://4630ce24-85ef-4b8c-a8c6-64872ccbbf1e)

Y entrando en el detalle del deployment podremos acceder a los pods, escalarlos, ver los logs, abrir un terminal, etc, etc…

![image](media://26c87297-d859-4c74-8601-9d148a43bae6)

- **Service**: Para crear servicios y permitir que nuestro despliegue sea accesible por otros deployments tendremos que ir hasta la sección *Networking → Services*, y de la misma manera que con los Deployment, crearemos el servicio copiando y pegando nuestro manifest.

![image](media://6eb7f763-3328-4ad4-9cfc-7b0f5f2d25ee)

- **Secrets**: Exactamente igual que los Deployments, accesible desde la opción Workloads → Secrets

![image](media://191b09e9-393b-40bb-a105-145ca6dd0071)

- **Persistent Volume Claim**: En el caso de la persistencia podremos hacer uso por completo de la UI para crearlos sin necesidad de manifest, aunque es recomendable tener el manifest como veremos posteriormente para poder aplantillarlo con Helm

![image](media://4dda3796-4af9-4edc-b882-e3963498a69c)

Seleccionaremos el modo de acceso (RWX, RWO o ROX) y el tamaño

![image](media://78725213-b1c6-486d-86a1-e1c643c5ae63)

- **Routes / Ingress**: En el caso de que queramos exponer nuestro servicio fuera del cluster podremos hacerlo mediante un Ingress o un Route, este último es exclusivo de Openshift, y nos permitirá de manera visual crear un endpoint a nuestro Servicio.

![image](media://2c942fb5-fa48-4b54-8b5c-dbd023dc9fb2)

Podremos elegir el contexto al que acceder, el servicio, puerto, y si queremos securizarlo (necesitaremos el certificado con wildcard del dominio del cluster y su clave privada)

![image](media://9b61c355-83f4-423e-9309-6fb34ee9f666)

### Aplantillamiento de manifests con Helm

Los pasos anteriores para desplegar nuestro software basado en distintos módulos/deployments podremos hacerlo como hemos explicado en el punto anterior (desde la UI de Openshift o Kubernetes) desplegando cada módulo por separado, además de desplegar Deployments, Services, Secrets, Persistent Volumen Claim por separado, o podremos desplegarlo todo “de golpe“ con Helm. Helm nos permite distribuir nuestro software desplegado en Kubernetes de manera sencilla y sobre todo versionable y fácil de actualizar. Helm permite aplantillar los Manifest de Kubernetes y empaquetarlos para que puedan ser distribuidos mediante un servidor de Charts (chart museum, monocular, etc…)  De esta manera si nuestro software está basado en varios módulos será mucho más sencillo desplegarlos y actualizarlos de una única que vez que tenerlos que desplegar a mano uno a uno.

La estructura básica de Helm, por ejemplo, es la siguiente (se puede generar con el comando **helm create <chart_name>**)

```shell
+- chart/
   +- templates/
      +- module/
         - deployment.yml
         - service.yml  
      - _helpers.tpl
      - NOTES.txt       
   - Chart.yml
   - values.yml
   - .helmignore
```

- Donde el **Chart.yml **contiene la meta-información de nuestro Chart, tales como: nombre, descripción, versión del chart, versión del software que lo contiene, tipo de Chart, etc…
- El fichero **values.yml** contiene todas aquellas variables que vayamos a querer parametrizar, por ejemplo, tags de nuestras imágenes, cadenas de conexión a base de datos, número de réplicas de los pods, etc… este fichero se puede sobre escribir al desplegar nuestro Chart en Kubernetes.
- **.helmignore**, al igual que con git, es un fichero donde podemos indicar todos los ficheros que no queremos incluir en nuestro Chart.
- Dentro del directorio templates tendremos las plantillas de cada uno de nuestros módulos, por ejemplo el **deployment.yml** y el **service.yml**.
- A este mismo nivel encontramos el fichero **_helpers.tpl** , donde podemos definir código en Go que podremos incluir en nuestras plantillas.

Un ejemplo de un fichero de values.yml sería este:

```yaml
# Variables for configdb deployment/service/storage
configdb:
  nameOverride: configdb
  image:
    repository: registry.onesaitplatform.com/onesaitplatform/configdb
    tag: mariadb
    pullPolicy: Always
  service:
    type: ClusterIP
    port: 3306
    targetPort: 3306
  storage:
    size: 1Gi
```

Los valores de ese fichero se podrían usar tanto el deployment.yml de Kubernetes como en el service.yml de esta manera: **.Values.configdb.image.repository** que hace referencia a la dirección del registro de la imagen de ese deployment (registry.onesaitplatform.com/onesaitplatform/configdb) mientras que **.Values.configdb.image.tag** haria referencia al tag empleado (mariadb)

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "onesait-platform.configdb.name" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    app: {{ include "onesait-platform.configdb.name" . }}
spec:
  replicas: {{ .Values.global.replicaCount }}
  selector:
    matchLabels:
      app: {{ include "onesait-platform.configdb.name" . }}
  template:
    metadata:
      labels:
        app: {{ include "onesait-platform.configdb.name" . }}
    spec:
      containers:
      - name: {{ include "onesait-platform.configdb.name" . }}
        image: "{{ .Values.configdb.image.repository }}:{{ .Values.configdb.image.tag }}"
        imagePullPolicy: {{ .Values.configdb.image.pullPolicy }}
        ports:
        - containerPort: {{ .Values.configdb.service.targetPort }}
          name: 3306tcp33062
          protocol: TCP
        volumeMounts:
        - mountPath: /var/lib/mysql
          name: configdb-data
          subPath: configdb
      restartPolicy: Always
      volumes:
      - name: configdb-data
        persistentVolumeClaim:
          claimName: configdb-pvc          
```

En las plantillas también puede incluirse código en go con la directiva **include** y apoyándonos en el fichero de **_helpers.tpl**, en el ejemplo anterior el nombre del deployment lo generamos en Go, incluyendo la variable **include "onesait-platform.configdb.name" **definida en _helpers.tpl:

```yaml
{{- define "onesait-platform.configdb.name" -}}
{{- default .Chart.Name .Values.configdb.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
```

Para evitar problemas con nombres de despliegues muy largos, hacemos un truncate del nombre a 63 caracteres, que es el máximo permitido por Kubernetes.

### Empaquetar un Chart

Una vez tengamos nuestro Chart definido, antes de empaquetarlo y distribuirlo podremos validar que es sintácticamente correcto con la instrucción lint de Helm:

```shell
> helm lint ./chart
```

```shell
==> Linting ./chart/

1 chart(s) linted, 0 chart(s) failed
```

Una vez que nos hayamos asegurado que no tiene errores podremos empaquetarlo y subirlo a nuestro repositorio de Charts (Chart Museum o Monocular)

- Lo empaquetamos primero:

```shell
> helm package ./chart
```

- Si tenemos un servidor de Charts y un plugin para subir el chart al servidor de charts lo podríamos hacer así:

```shell
helm push chart/ <chart_repository> --username <user> --password <pass>
```

### Despliegue y rollout de un Chart en Kubernetes/Openshift

Ya con el chart empaquetado podremos desplegarlo en nuestro cluster de Kubernetes/Openshift, previamente tendremos que tener configurado nuestro fichero **.kube/config** en k8s con los datos de acceso al cluster o haber hecho **oc login** al cluster de Openshift.

Para desplegar nuestro aplicativo o instalar el chart lo haremos con el siguiente comando:

- Si usamos un servidor de charts, tendremos que añadir y/o actualizar nuestro repositorio local:

```shell
> helm repo add <nombre_repo_local> <url_repositorio_remoto> --username <user> --password <password>
```

```shell
> helm repo update
```

- Instalar el chart:

```shell
> helm install <nombre_repo_local>/<nombre_chart> \
               --namespace <namespace> \
               --generated-name \
               --version <version_del_chart>
```

- Para sobre escribir algún valor del fichero values.yml podemos usar el flag --set y la estructura completa de la variable dentro del yml:

```shell
> helm install <nombre_repo_local>/<nombre_chart> \
               --namespace <namespace> \
               --generated-name \
               --set global.env.realtimeDbServers=172.23.155.34:27017 \
               --version <version_del_chart>
```

- O podemos usar nuestro propio fichero de valores que sobre escriban el del propio Chart:

```shell
> helm install <nombre_repo_local>/<nombre_chart> \
               -f custom-values.yml \
               --namespace <namespace> \
               --generated-name \
               --version <version_del_chart>
```

- Para listar nuestras instalaciones de Charts activas:

```shell
> helm list --namespace <namespace>
```

- Para actualizar un despliegue existente con una nueva versión de nuestro aplicativo bastará ejecutar el comando **upgrade**, indicando el nombre de la instalación, el namespace de Kubernetes/Openshift y la versión del Chart

```yaml
> helm upgrade <deployment_name> <chart_name> --namespace <namespace_name> --version <chart_version>
```

- Para desinstalar por completo nuestro aplicativo, usaremos el comando **uninstall**:

```yaml
> helm uninstall <generated_name>
```