Elaboramos la tarea de despliegue en GKE sin plugins, SMS y registro. Echamos un vistazo a Jenkins bajo el saco.

Todo comenzó cuando el líder de uno de nuestros equipos de desarrolladores solicitó que se publicara en modo de prueba su nueva aplicación, que había sido containerizada el día anterior. La publiqué. Aproximadamente 20 minutos después, recibí una solicitud para actualizar la aplicación, porque habían añadido una función muy necesaria. Actualicé. Un par de horas más tarde… bueno, ya se imaginan lo que sucedió después...

Confieso que soy bastante perezoso (¿no lo he admitido antes? ¿no?), y, teniendo en cuenta que los líderes de equipo tienen acceso a Jenkins, donde tenemos todo nuestro CI/CD, pensé: ¡déjame que él despliegue tanto como quiera! Recordé un chiste: dale un pez a un hombre y comerá un día; nómbralo Saciado y comerá toda su vida. Y me fui a crear un trabajo, que pudiera desplegar en un contenedor de Kubernetes con cualquier versión de aplicación que se haya construido con éxito y pasarle cualquier valor ENV (mi abuelo, filólogo y profesor de inglés en el pasado, ahora giraría su dedo en la sien y me miraría con expresiones elocuentes al leer esta oración).

Así que, en esta nota contaré cómo aprendí:

  1. Actualizar dinámicamente tareas en Jenkins desde la misma tarea o desde otras tareas;
  2. Conectarse a la consola en la nube (Cloud shell) desde un nodo con un agente de Jenkins instalado;
  3. Desplegar cargas de trabajo en Google Kubernetes Engine.


En realidad, estoy siendo un poco astuto. Se supone que al menos parte de la infraestructura está en la nube de Google, por lo tanto, eres usuario de ella y, por supuesto, tienes una cuenta de GCP. Pero esta nota no trata de eso.

Esta es otra de mis chuletas. Escribo estas notas solo en un caso: me enfrento a una tarea, inicialmente no sé cómo resolverla, la solución no se encuentra en Google de forma lista, así que la busco por partes y al final resuelvo la tarea. Y para que en el futuro, cuando olvide cómo lo hice, no tenga que volver a buscar todo por partes y compilarlo todo junto, escribo estas chuletas.

Descargo de responsabilidad: 1. Esta nota fue escrita "para mí", no pretende ser una buena práctica. Con gusto leeré opciones de "hubiera sido mejor hacerlo así" en los comentarios.
2. Si consideramos la parte práctica de la nota como sal, esta, al igual que todas mis notas anteriores, es una solución débilmente salina.

Actualización dinámica de la configuración de trabajos en Jenkins

Anticipo tu pregunta: ¿qué tiene que ver la actualización dinámica del trabajo? ¡Escribí manualmente el valor del parámetro de cadena y adelante!

Te responderé: realmente soy perezoso, no me gusta cuando alguien se queja: ¡Misha, el despliegue falla, todo se ha perdido! Comienzas a revisar y ahí hay un error tipográfico en el valor de algún parámetro de lanzamiento del trabajo. Por eso prefiero hacer todo lo más a prueba de fallos posible. Si hay una forma de evitar que el usuario introduzca datos directamente, ofreciendo en su lugar una lista de valores para elegir, entonces organizo la selección.

El plan es el siguiente: creamos un trabajo en Jenkins, en el cual antes de ejecutar se puede seleccionar la versión de una lista, indicar los valores para los parámetros que se pasan al contenedor a través de ENV, luego recoge el contenedor y lo envía al Container Registry. Desde allí, el contenedor se inicia en Kubernetes como workload con los parámetros definidos en el trabajo.

No vamos a considerar el proceso de creación y configuración de un trabajo en Jenkins, eso sería un tema fuera de lugar. Asumiremos que el trabajo está listo. Para implementar una lista actualizable con versiones, necesitamos dos cosas: una lista de origen ya existente con números de versión válidos y una variable del tipo Choice parameter en el trabajo. En nuestro ejemplo, supongamos que la variable se llama BUILD_VERSION, no profundizaremos en ella. Pero detengámonos un poco más en la lista de origen.

Las opciones no son tantas. Me vienen a la mente dos de inmediato:

  • Utilizar la API de acceso remoto que Jenkins ofrece a sus usuarios;
  • Solicitar el contenido de una carpeta remota del repositorio (en nuestro caso, esto es JFrog Artifactory, lo cual no es esencial).

Jenkins Remote access API

Por la hermosa tradición que se ha establecido, prefiero evitar explicaciones largas.
Solo me permitiré una traducción libre de un fragmento del primer párrafo de la primera página de la documentación de la API.:

Jenkins proporciona una API para el acceso remoto comprensible por máquina a su funcionalidad. El acceso remoto se ofrece en un estilo similar a REST. Esto significa que no hay un único punto de entrada para todas las capacidades, sino que se utiliza una URL del tipo ".../api/", donde "…" indica el objeto al cual se aplican las capacidades de la API.

En otras palabras, si el trabajo de despliegue, del que hablamos actualmente, está disponible en la dirección http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, la API para esta tarea está disponible en http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/

A continuación, tenemos la opción de recibir la salida en un formato. Nos detendremos en XML, ya que la API solo permite filtro en este caso.

Intentemos simplemente obtener una lista de todas las ejecuciones de la tarea. Solo nos interesa el nombre de la compilación (displayName) y su resultado (resultado):

http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]

¿Se logró?

Ahora filtraremos solo aquellas ejecuciones que tengan como resultado ÉXITO. Usaremos el argumento &exclude y como parámetro le pasaremos la ruta que no es igual a ÉXITO. Sí, sí. La doble negación es una afirmación. Excluimos todo lo que no nos interesa:

http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS']

Captura de pantalla de la lista de éxitos
Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

Y solo para asegurarnos de que el filtro no nos haya engañado (¡los filtros nunca mienten!) y mostraremos la lista de 'no exitosos':

http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS']

Captura de pantalla de la lista de no exitosos
Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

Lista de versiones de la carpeta en el servidor remoto

Hay una segunda forma de obtener la lista de versiones. Me gusta incluso más que llamar a la API de Jenkins. Porque si la aplicación se compiló correctamente, significa que se empacó y se puso en el repositorio en la carpeta correspondiente. Básicamente, el repositorio es por defecto un almacenamiento de versiones de trabajo de las aplicaciones. Así que veamos qué versiones tiene almacenadas. Vamos a usar curl, grep y awk. Si a alguien le interesa un one-liner, está bajo el spoiler.

Comando en una línea
Tenga en cuenta dos cosas: estoy pasando las credenciales de conexión en la cabecera y no necesito todas las versiones de la carpeta, y estoy filtrando solo aquellas que fueron creadas en el último mes. Edite el comando según sus realidades y necesidades:

curl -H "X-JFrog-Art-Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>K[^/]+ )

Configuración de tareas y archivo de configuración de la tarea en Jenkins

Hemos resuelto el tema de la fuente de la lista de versiones. Ahora integremos la lista obtenida en la tarea. Para mí, la solución obvia fue agregar un paso en la tarea para compilar la aplicación. Un paso que se llevaría a cabo en caso de un resultado de 'éxito'.

Abrimos la configuración de la tarea de compilación y desplazamos hacia abajo. Hacemos clic en los botones: Add build step -> Conditional step (single). En la configuración del paso, elegimos la condición Current build status, establecemos el valor ÉXITO, acción a realizar en caso de éxito Run shell command.

Y ahora lo más interesante. Las configuraciones de tareas de Jenkins se almacenan en archivos. En formato XML. En la ruta http://ruta-a-la-tarea/config.xml Por lo tanto, se puede descargar el archivo de configuración, editarlo como sea necesario y colocarlo en su lugar de origen.

Recuerden, antes acordamos que para la lista de versiones crearemos un parámetro BUILD_VERSION?

Descarguemos el archivo de configuración y echemos un vistazo dentro. Solo para asegurarnos de que el parámetro está en su lugar y realmente tiene el formato adecuado.

Captura de pantalla debajo del spoiler.

Su fragmento proporcionado de config.xml debería verse así. Con la excepción de que el contenido del elemento choices aún está ausente.
Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

¿Lo comprobaron? Bien, ahora escribimos un script que se ejecutará en caso de que la compilación sea exitosa.
El script obtendrá la lista de versiones, descargará el archivo de configuración, escribirá en él la lista de versiones en el lugar que necesitamos y luego lo volverá a colocar. Sí, es correcto. Escribir la lista de versiones en el XML en el lugar donde ya existe la lista de versiones (estará en el futuro, después de la primera ejecución del script). Sé que en el mundo aún existen fervientes aficionados a las expresiones regulares. No me incluyo entre ellos. Por favor, instalen xmlstarlet en la máquina donde se editará la configuración. Creo que no es un gran precio a pagar para evitar editar XML con sed.

Debajo del spoiler incluyo el código que realiza toda la secuencia descrita anteriormente.

Escribimos en la configuración la lista de versiones desde la carpeta en el servidor remoto

#!/bin/bash
############## Скачиваем конфиг
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml

############## Удаляем и заново создаем xml-элемент для списка версий
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml

xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml

xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml

############## Читаем в массив список версий из репозитория
readarray -t vers < <( curl -H "X-JFrog-Art-Api:Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>K[^/]+' )

############## Пишем массив элемент за элементом в конфиг
printf '%sn' "${vers[@]}" | sort -r | 
                while IFS= read -r line
                do
                    xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
                done

############## Кладем конфиг взад
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml

############## Приводим рабочее место в порядок
rm -f appConfig.xml

Si prefieres la opción de obtener versiones de Jenkins y eres tan perezoso como yo, debajo del spoiler está el mismo código, pero la lista es de Jenkins:

Escribimos en la configuración la lista de versiones desde Jenkins
Solo ten en cuenta que el nombre de mi compilación consiste en un número ordinal y un número de versión, separados por dos puntos. Por lo tanto, awk corta la parte innecesaria. Modifica esta cadena según tus necesidades.

#!/bin/bash
############## Скачиваем конфиг
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml

############## Удаляем и заново создаем xml-элемент для списка версий
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml

xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml

xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml

############## Пишем в файл список версий из Jenkins
curl -g -X GET -u username:apiKey 'http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!=%22SUCCESS%22]&pretty=true' -o builds.xml

############## Читаем в массив список версий из XML
readarray vers < <(xmlstarlet sel -t -v "freeStyleProject/allBuild/displayName" builds.xml | awk -F":" '{print $2}')

############## Пишем массив элемент за элементом в конфиг
printf '%sn' "${vers[@]}" | sort -r | 
                while IFS= read -r line
                do
                    xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
                done

############## Кладем конфиг взад
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml

############## Приводим рабочее место в порядок
rm -f appConfig.xml

En teoría, si has probado el código escrito basado en los ejemplos anteriores, ya deberías tener un menú desplegable con versiones en la tarea de despliegue. Aproximadamente como en la captura de pantalla debajo del spoiler.

Lista de versiones correctamente completada
Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

Si todo funcionó, copia y pega el script en Run shell command y guarda los cambios.

Conexión a Cloud shell

Los recolectores están en nuestros contenedores. Como medio para entregar aplicaciones y gestor de configuraciones, utilizamos Ansible. Por lo tanto, cuando se trata de construir contenedores, se me ocurren tres opciones: instalar Docker en Docker, instalar Docker en una máquina con Ansible, o construir contenedores en la consola en la nube. Sobre los complementos para Jenkins, acordamos no mencionar nada en esta nota. ¿Recuerdas?

Decidí: bueno, dado que los contenedores se pueden construir 'out of the box' en la consola en la nube, ¿para qué complicarse? Mantenerlo limpio, ¿verdad? Quiero construir contenedores con Jenkins en la consola en la nube, y luego lanzarlos en Kubernetes desde allí. Más aún, dada la infraestructura de Google, que cuenta con canales muy anchos, esto beneficiará la velocidad de despliegue.

Para conectarse a la consola en la nube se necesitan dos cosas: gcloud y permisos de acceso a Google Cloud API para la instancia de VM desde la cual se realizará la conexión.

Para aquellos que planean conectarse desde fuera de la nube de Google.
Google permite deshabilitar la autenticación interactiva en sus servicios. Esto permitirá conectarse a la consola incluso desde una máquina de café, siempre que esté bajo *nix y tenga su propia consola.

Si hay necesidad de que trate este tema con más detalle en esta nota, déjenme saber en los comentarios. Si consigo suficientes votaciones, escribiré una actualización al respecto.

La forma más sencilla de otorgar permisos es a través de la interfaz web.

  1. Detén la instancia de la VM desde la cual se ejecutará la conexión a la consola en la nube más adelante.
  2. Abre la Información de la instancia y haz clic en Modificar.
  3. En la parte inferior de la página, selecciona el ámbito de acceso de la instancia. Acceso completo a todas las Cloud API.

    Captura de pantalla
    Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

  4. Guarda los cambios y reinicia la instancia.

Una vez que la VM haya cargado, conéctate a ella por SSH y asegúrate de que la conexión se realiza sin errores. Usa el comando:

gcloud alpha cloud-shell ssh

Una conexión exitosa se verá algo así:
Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

Despliegue en GKE

Dado que estamos buscando completamente migrar a IaC (Infraestructura como Código), nuestros Dockerfiles se almacenan en Git. Por un lado. El despliegue en Kubernetes se describe en un archivo YAML que se usa solo para esta tarea, lo cual también es, de alguna manera, código. Por el otro lado. En resumen, mi plan es el siguiente:

  1. Tomamos los valores de las variables BUILD_VERSION y, opcionalmente, los valores de las variables que se transmitirán a través de ENV.
  2. Descargamos el Dockerfile desde Git.
  3. Generamos un YAML para el despliegue.
  4. Subimos ambos archivos mediante SCP a la consola en la nube.
  5. Construimos el contenedor allí y lo enviamos al Container registry
  6. Aplicamos el archivo de despliegue de carga en Kubernetes.

Seamos más concretos. Dado que hemos hablado de ENV, supongamos que necesitamos transmitir los valores de dos parámetros: PARAM1 y PARAM2. Agregamos su asignación al despliegue, tipo — String Parameter.

Captura de pantalla
Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

Generaremos el YAML mediante una simple redirección echo a un archivo. Suponemos, por supuesto, que en el Dockerfile tiene que estar presente PARAM1 y PARAM2, que el nombre de la carga será awesomeapp, y que el contenedor compilado con la versión indicada se encuentra en Container registry en la ruta gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION, donde $BUILD_VERSION y fue seleccionado de la lista desplegable.

Listado de comandos

touch deploy.yaml
echo "apiVersion: apps/v1" >> deploy.yaml
echo "kind: Deployment" >> deploy.yaml
echo "metadata:" >> deploy.yaml
echo "  name: awesomeapp" >> deploy.yaml
echo "spec:" >> deploy.yaml
echo "  replicas: 1" >> deploy.yaml
echo "  selector:" >> deploy.yaml
echo "    matchLabels:" >> deploy.yaml
echo "      run: awesomeapp" >> deploy.yaml
echo "  template:" >> deploy.yaml
echo "    metadata:" >> deploy.yaml
echo "      labels:" >> deploy.yaml
echo "        run: awesomeapp" >> deploy.yaml
echo "    spec:" >> deploy.yaml
echo "      containers:" >> deploy.yaml
echo "      - name: awesomeapp" >> deploy.yaml
echo "        image: gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION:latest" >> deploy.yaml
echo "        env:" >> deploy.yaml
echo "        - name: PARAM1" >> deploy.yaml
echo "          value: $PARAM1" >> deploy.yaml
echo "        - name: PARAM2" >> deploy.yaml
echo "          value: $PARAM2" >> deploy.yaml

Al agente de Jenkins, después de conectarse mediante gcloud alpha cloud-shell ssh no se puede acceder al modo interactivo, por lo que transmitimos los comandos a la consola en la nube usando el parámetro —command.

Limpiamos la carpeta de inicio en la consola en la nube de Dockerfile antiguo:

gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"

Colocamos el Dockerfile descargado recientemente en la carpeta de inicio de la consola en la nube mediante SCP:

gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~

Construimos, etiquetamos y enviamos el contenedor al Container registry:

gcloud alpha cloud-shell ssh --command="docker build -t awesomeapp-$BUILD_VERSION ./ --build-arg BUILD_VERSION=$BUILD_VERSION --no-cache"
gcloud alpha cloud-shell ssh --command="docker tag awesomeapp-$BUILD_VERSION gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"
gcloud alpha cloud-shell ssh --command="docker push gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"

Procedemos de manera similar con el archivo de despliegue. Tenga en cuenta que en los comandos a continuación se utilizan nombres ficticios del clúster, donde se realiza el despliegue (awsm-cluster) y el nombre del proyecto (awesome-project), donde se encuentra el clúster.

gcloud alpha cloud-shell ssh --command="rm -f deploy.yaml"
gcloud alpha cloud-shell scp localhost:./deploy.yaml cloudshell:~
gcloud alpha cloud-shell ssh --command="gcloud container clusters get-credentials awsm-cluster --zone us-central1-c --project awesome-project && 
kubectl apply -f deploy.yaml"

Iniciamos la tarea, abrimos la salida de la consola y esperamos ver una compilación exitosa del contenedor.

Captura de pantalla
Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

Y luego, un despliegue exitoso del contenedor generado.

Captura de pantalla
Creamos una tarea de despliegue en GKE sin plugins, SMS ni registro. Echamos un vistazo furtivo a Jenkins.

Intencionadamente omití la configuración Ingress. Por una sencilla razón: una vez configurado con un workload nombre determinado, seguirá funcionando sin importar cuántos despliegues se realicen con ese nombre. Y, en general, esto está un poco fuera del tema.

En lugar de conclusiones

Probablemente todos los pasos anteriores se podían omitir y simplemente instalar algún plugin para Jenkins, hay millones. Pero por alguna razón no me gustan los plugins. Bueno, más bien, solo recurro a ellos en caso de necesidad.

Y también me gusta explorar un nuevo tema. El texto anterior es en parte una forma de compartir los descubrimientos que hice al resolver la tarea descrita al principio. Compartir con aquellos que no son tan experimentados en DevOps. Si mis hallazgos ayudan a alguien, estaré satisfecho.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster