OpenShift: Remote Debug

Por suerte o por desgracia el debug remoto es algo que todos hemos tenido que llevar acabo en algún momento de nuestras vidas, en entornos no productivos espero.

En plataformas como Kubernetes o OpenShift esto cobra especial sentido cuando se desarrolla sobre las distribuciones Minikube o Minishift y en el presente articulo se pretende detallar como llevarlo a cabo, paso a paso, de forma sencilla.

Caso de uso

Se dispone en OpenShift de un microservicio Java desarrollado con Spring Boot del que se quiere realizar debug remoto.

Port Forwarding

Port forwarding es una técnica que permite reenviar el trafico de un puerto local (de nuestro PC), al puerto de un pod de OpenShift. Es decir, permite escuchar y enviar datos del pod, lo que a su vez abre las puertas al debug remoto.

El comando oc para realizar el port forwarding sigue la siguiente estructura:

oc port-forward pod puerto_local:puerto_pod

Llevado al presente caso de uso, el comando tendría la siguiente estructura:

oc port-forward spring-microservice-51-aj9la 8080:8080

Si todo ha salido correctamente al realizar una petición sobre http://localhost:8080 se debería obtener una respuesta por parte del microservicio.

Java Remote Debug

Para habilitar debug remoto sobre la JVM de una aplicación Java es necesario asignarle una serie de parámetros en el arranque.

-Xdebug -agentlib:jdwp=transport=dt_socket,address=DEBUG_PORT,server=y,suspend=n

En OpenShift, una de las opciones de llevarlo a cabo es definir una variable de entorno “JAVA_OPTIONS” que contenga dichos valores. Esta variable puede ser definida en el DeploymentConfig de la siguiente manera:

    spec:
      containers:
        - env:
            - name: JAVA_OPTIONS
              value: >-
                -Xdebug
                -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=n


Una vez añadida la variable “JAVA_OPTIONS” es hora de realizar el
port forwarding sobre el puerto de debug remoto establecido en la misma. Sobra decir que no puede coincidir con el puerto sobre el que escucha el microservicio.

oc port-forward spring-microservice-51-aj9la 8888:8888

Finalmente solo queda realizar el debug remoto como en cualquier aplicación Java convencional desplegada fuera de OpenShift.

En el IDE Eclipse por ejemplo, basta con definir una configuración de debug de tipo “Remote Java Application” y referenciar la dirección en cuestión (http://localhost:8888)


Conclusiones

En conclusión, gracias al port forwarding es posible escuchar y enviar datos de un pod desplegado en OpenShift en un puerto local, que es lo que habilita la opción de realizar debug en remoto.

Advertisements

OpenShift: ConfigMaps & Secrets

Históricamente cuando una aplicación requería de datos de configuración, como por ejemplo, la cadena de conexión a una base de datos o el nivel de granularidad de los logs, no quedaba más remedio que empaquetarlos dentro de la propia aplicación, pasarlos como argumentos de linea de comandos o a través de variables del sistema. La primera de las opciones requería construir y desplegar una nueva versión de la aplicación, mientras que en el resto no existía un versionado o histórico en un SCM.

En los últimos años, con la irrupción de los contenedores (Docker en su gran mayoría) y a los sistemas de orquestación como Kubernetes, la manera en la que se gestionan los datos de configuración ha cambiado. El nuevo paradigma busca desacoplar la configuración del contenido de la imagen, manteniendo así la portabilidad de los contenedores. Es decir, la aplicación y la configuración se gestionan por separado.

OpenShift, mas bien Kubernetes, proporciona dos utilidades para ello.

ConfigMap

Un ConfigMap es un objecto de OpenShift/Kubernetes que proporciona un mecanismo para la inyección de datos de configuración en los contenedores. En su interior puede albergar información en formato clave-valor o estructuras complejas como ficheros properties, YAML o binarios.

Este objeto puede crearse tanto desde la consola de administración de OpenShift como desde linea de comando con el OpenShift Container Platform CLI y a modo de ejemplo, este sería aspecto de un ConfigMap con varios ficheros de configuración en su interior:

apiVersion: v1
metadata:
  name: application-config
data: 
  server.port: 8080
  application.properties: |-
    spring.application.name = "Demo application"
  application.yml: |-    
    spring:
      application:
        admin:
          enabled: false
binaryData:
  bar: L3Jvb3QvMTAw

En resumen, es posible detallar los datos configuración de una aplicación en un objeto JSON|YAML, que después pueden ser cargados en OpenShift y añadidos al contenedor en fase de despliegue, manteniendo ambas partes desacopladas.

Una buena forma de hacerlo es almacenar la definición de un ConfigMap como código en un SCM, lo que brinda opciones como el versionado o auditoria. El SCM a su vez, puede notificar a Jenkins con cada nuevo cambio y actualizar el ConfigMap de OpenShift vía job.

pipeline {
	agent any
	stages { 
		stage('Checkout') {
			steps {
				git url: "$BITBUCKET_URL", credentialsId: 'bbCredentials', branch: 'master'
			}
		}    
		stage('Update ConfigMap') {
			steps {
				script {
					// ConfigMap is updated if file exists but is never deleted in Openshift
					if (fileExists('./configmap.yml')) {
						sh '''
							oc login $OPENSHIFT_URL --token=$OPENSHIFT_TOKEN
							sh 'oc apply -f ./configmap.yml -n $OPENSHIFT_PROJECT'
						'''
					}
				}
			}
		}
	}
    post {
		always {   
			cleanWs()
		}  
	}  
}

 Una vez que el ConfigMap esta creado en OpenShift, es el momento de decidir de que manera se quiere inyectar la información en el contenedor.

Environment Variables

Una opción es añadir las propiedades clave-valor definidas en el ConfigMap como variables de entorno del contenedor.

Partiendo del siguiente ConfigMap:

kind: ConfigMap
apiVersion: v1
metadata:
  name: application-config
data:
  server.port: 8080
  spring.application.name = "Demo application"

Es posible especificar una a una las propiedades especificas a cargar como variables de entorno en la configuración del despliegue (DeploymentConfig).

spec:
  containers:
  - name: demo-application 
    image: registry/demo-application 
    env:  
    - name: SERVER_PORT
      valueFrom:
        configMapKeyRef:
          name: application-config 
          key: server.port

O cargar directamente todos los valores existentes en el ConfigMap.

spec:
  containers:
  - name: demo-application 
    image: registry/demo-application
    envFrom: 
      configMapRef:
        name: application-config

Volumes

La otra opción es consumir el ConfigMap como un volumen, es decir, inyectando físicamente el fichero properties|YAML|binary en una ruta del contenedor.

Partiendo del siguiente ConfigMap:

kind: ConfigMap
apiVersion: v1
metadata:
  name: application-config
  data: 
    application.yml: |-
      spring:
        application:
          admin:
            enabled: false
          name: "Demo application"

La configuración del DeploymentConfig sería la siguiente:

spec:
  containers:
  - name: demo-application 
    image: registry/demo-application 
    volumeMounts: 
    - name: application-config
      mountPath: /deployments/config/application
  volumes:
  - name: application-config
    configMap:
      name: application-config 

De esta manera, se consigue inyectar físicamente en la fase de despliegue, el fichero “application.yml” definido en el ConfigMap, en la ruta “/deployments/config/application” del contenedor.

Secret

Un Secret es un objecto de OpenShift/Kubernetes que proporciona un mecanismo para la inyección de datos de configuración sensibles en los contenedores como pueden ser credenciales, ficheros dockercfg, certificados etc. Es decir, se trata de un ConfigMap especialmente diseñado para información sensible.

Este objeto puede crearse tanto desde la consola de administración de OpenShift como desde linea de comando con el OpenShift Container Platform CLI y a modo de ejemplo, este sería aspecto de un Secret 
con distintos ficheros de configuración en su interior:

kind: Secret
metadata:
  name: application-sensitive-config
  type: Opaque
  data:
    ORACLE_URL: amRiYzpvcmFjbGU6dGhpbjpALy9iZC5vcmFjbGUuY29tOjUwNzE1L0VYQU1QTEU=
    ORACLE_USERNAME: dXNlcm5hbWU=
    ORACLE_PASSWORD: cGFzc3dvcmQ=
  stringData:
    secret.properties: |-
      property1=valueA
      property2=valueB

Del mismo modo que un ConfigMap, esto permite detallar los datos configuración de una aplicación en un objeto JSON|YAML, que después pueden ser cargados en OpenShift y añadidos al contenedor en fase de despliegue, manteniendo ambas partes desacopladas.

En consecuencia, también es posible almacenarlo como código en un SCM (versionado, auditoria) y que este a su vez notifique a Jenkins con cada nuevo cambio para actualizarlo en OpenShift vía job.

pipeline {
	agent any
	stages { 
		stage('Checkout') {
			steps {
				git url: "$BITBUCKET_URL", credentialsId: 'bbCredentials', branch: 'master'
			}
		}    
		stage('Update Secret') {
			steps {
				script {
					// Secret is updated if file exists but is never deleted in Openshift
					if (fileExists('./secret.yml')) {
						sh '''
							oc login $OPENSHIFT_URL --token=$OPENSHIFT_TOKEN
							sh 'oc apply -f ./secret.yml -n $OPENSHIFT_PROJECT'
						'''
					}
				}
			}
		}
	}
    post {
		always {   
			cleanWs()
		}  
	}  
}

Una vez que el Secret esta creado en OpenShift, es el momento de decidir de que manera se quiere inyectar la información en el contenedor y la posibilidades vuelven a ser las mismas.

Environment Variables

Una opción es añadir las propiedades clave-valor definidas en el Secret como variables de entorno del contenedor.

Partiendo del siguiente ConfigMap:

apiVersion: v1
kind: Secret
metadata:
  name: application-sensitive-config
  type: Opaque
  data:
    ORACLE_URL: amRiYzpvcmFjbGU6dGhpbjpALy9iZC5vcmFjbGUuY29tOjUwNzE1L0VYQU1QTEU=
    ORACLE_USERNAME: dXNlcm5hbWU=
    ORACLE_PASSWORD: cGFzc3dvcmQ=

Es posible especificar una a una las propiedades especificas a cargar como variables de entorno en la configuración del despliegue  (DeploymentConfig).

spec:
  containers:
  - name: demo-application
    image: registry/demo-application 
    env:
    - name: ORACLE_URL
        valueFrom:
          secretKeyRef:
            name: application-sensitive-config
            key: ORACLE_URL 

O cargar directamente todos los valores existentes en el Secret.

spec:
  containers:
  - name: demo-application
    image: registry/demo-application
    envFrom: 
      secretRef:
       name: application-sensitive-config 

Volumes

La otra opción es consumir el Secret como un volumen, es decir, inyectando físicamente el fichero properties|YAML|binary en una ruta del contenedor.

Partiendo del siguiente Secret:

apiVersion: v1
  kind: Secret
  metadata:
    name: application-sensitive-config
  type: Opaque
  stringData:
    secret.properties: |-     
      property1=valueA
      property2=valueB

La configuración del DeploymentConfig sería la siguiente:

spec:
  containers:
  - name: demo-application
    image: registry/demo-application
    volumeMounts: 
    - name: application-sensitive-config
      mountPath: /deployments/config/application-sensitive-config
      readOnly: true
  volumes:
  - name: application-sensitive-config
    secret:
      secretName: application-sensitive-config 

De esta manera, se consigue inyectar físicamente en la fase de despliegue, el fichero “secret.properties” definido en el Secret, en la ruta “/deployments/config/application-sensitive-config” del contenedor.

Conclusiones

En conclusión, con los ConfigMaps y Secrets se logra desacoplar la configuración de la aplicación del contenido de la imagen, manteniendo así la portabilidad de los contenedores. Finalmente en la fase de despliegue se unen ambas partes.

Fuente

Ademas, gracias a su naturaleza (JSON|YAML) es posible almacenarlos como código en un SCM (de nuevo, histórico y auditoria) e integrar su gestión en los ciclos de integración continua.

Para acabar, comentar que aunque los cambios en un ConfigMap/Secret se reflejen en tiempo real en un contenedor, la aplicación debe estar preparada para volver a leer dichos valores en caliente.