Para desplegar ya no sólo Onesait Platform sino cualquier aplicativo contenerizado en clusters de Openshift o Kubernetes (k8s) es necesario seguir una serie de pautas o “Best Practices“, que van desde la fase de definición y construcción de tu imagen Docker hasta la fase de creación de manifests de Kubernetes, “aplantillamiento“ de los mismos y distribución de tu software como una pieza o paquete con Helm.
Prerrequisitos:
Docker instalado en local.
Helm v3 instalado en local.1
Cli de Kubernetes (kubectl) o cli de Openshift (oc)1 para poder comunicarnos y operar nuestros despliegues contra el cluster.
1Si usamos Openshift podremos descargar tanto el cli de OCP como Helm del enlace que aparece en la imagen inferior.
Construcción de la imagen Docker
Para desplegar una imagen Docker en Openshift debes ser especialmente cuidadosos con la seguridad, no arrancando tus 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, trata 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 tu aplicación dentro del contenedor: Evita lanzar tu contenedor con usuario root. Lo más habitual es que dé problemas, y por seguridad no conviene hacerlo. Para ello, crea un usuario, un grupo y da permisos a este usuario en los directorios que tenga permisos o en los que crees y le hagas 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, los hay de muchos tipos para cometidos muy distintos, 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)
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:
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.
labels: app: apimanagerservice group: onesait-platorm
Filtrado por labels en Openshift:
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.
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.
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á
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
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:
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) 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 “/“
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 uno a uno, podremos hacerlo desde la propia UI de Openshift de la siguiente manera:
Deployment: Desde la opción de menú Workloads → Deployment → Create Deployment
Y pegaremos nuestro manifest para posteriormente poder crearlo:
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.
Y entrando en el detalle del deployment podremos acceder a los pods, escalarlos, ver los logs, abrir un terminal, etc, etc…
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.
Secrets: Exactamente igual que los Deployments, accesible desde la opción Workloads → Secrets
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
Seleccionaremos el modo de acceso (RWX, RWO o ROX) y el tamaño
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.
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)
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>)
+- 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:
# 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)
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:
{{- 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:
> helm lint ./chart
==> 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:
> 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í:
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:
> helm repo add <nombre_repo_local> <url_repositorio_remoto> --username <user> --password <password>
> helm repo update
Instalar el chart:
> 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:
> 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:
> 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:
> 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
> helm upgrade <deployment_name> <chart_name> --namespace <namespace_name> --version <chart_version>
Para desinstalar por completo nuestro aplicativo, usaremos el comando uninstall:
> helm uninstall <generated_name>