Dominamos la tarea de implementación en GKE sin complementos, SMS y registro. Echa un vistazo debajo de la chaqueta de Jenkins con un ojo

Todo comenzó con el hecho de que un líder de equipo de uno de nuestros equipos de desarrollo pidió en modo de prueba exponer su nueva aplicación, que había sido contenida en contenedores el día anterior. Lo puse. Después de unos 20 minutos, se recibió una solicitud para actualizar la aplicación, porque allí se terminó una pieza muy necesaria. Renové. Después de un par de horas ... bueno,



ya adivinas lo que comenzó a suceder después ... Debo admitir, soy bastante vago (¿admití esto antes? ¿No?), Y, dado que los líderes de equipo tienen acceso a Jenkins, Tenemos todo el CI / CD, pensé: ¡que se despliegue todo lo que quiera! Recordé la anécdota: dale a un hombre un pescado y estará lleno por el día; llama a una persona saciada y será saciada toda su vida. Y se fue a jugar con el trabajo, que podría implementar un contenedor en un Kuber con la aplicación de cualquier versión ensamblada con éxito y transferirle cualquier valor ENV (mi abuelo, un filólogo, un profesor de inglés en el pasado, ahora torcía su dedo en su sien y me miraba muy expresivamente después de leer esto frase).



Entonces, en un post hablaré sobre cómo aprendí:



  1. Actualice dinámicamente trabajos en Jenkins desde el trabajo mismo o desde otros trabajos;
  2. Conéctese a la consola en la nube (shell de la nube) desde el nodo con el agente de Jenkins instalado;
  3. Implementa una carga de trabajo en Google Kubernetes Engine.


De hecho, soy, por supuesto, un poco astuto. Se asume que al menos parte de tu infraestructura está en la nube de google, y por lo tanto eres su usuario y, por supuesto, tienes una cuenta de GCP. Pero la nota no se trata de eso.



Esta es mi próxima hoja de trucos. Quiero escribir esas notas solo en un caso: tenía un problema antes que yo, inicialmente no sabía cómo resolverlo, la solución no estaba en Google en su forma final, así que la busqué en Google por partes y finalmente resolví el problema. Y para que en el futuro, cuando me olvide cómo lo hice, no tenga que buscar en Google todo de nuevo pieza por pieza y compilarlo, me escribo esas hojas de trucos.

Disclaimer: 1. « », best practice . « » .

2. , , , — .

Jenkins



Preveo su pregunta: ¿qué tiene que ver la actualización dinámica del trabajo con esto? Ingresé el valor del parámetro de cadena con los controladores y ¡adelante!



La respuesta es: soy muy vago, no me gusta cuando la gente se queja: Misha, la implementación se está cayendo, ¡todo se ha ido! Empieza a buscar y hay un error tipográfico en el valor de algún parámetro de ejecución de la tarea. Por lo tanto, prefiero hacer todo lo más completo posible. Si es posible evitar que el usuario ingrese datos directamente dando una lista de valores para seleccionar en su lugar, entonces organizo la selección.



El plan es el siguiente: crear un trabajo en Jenkins, en el que, antes del lanzamiento, sería posible seleccionar una versión de la lista, especificar valores para los parámetros pasados ​​al contenedor a través de ENV , luego recolecta el contenedor y lo empuja al Container Registry. Más allá de allí, el contenedor se lanza en kubera comocarga de trabajo con parámetros especificados en el trabajo.



No consideraremos el proceso de creación y configuración de un trabajo en Jenkins, esto es fuera de tema. Asumiremos que la tarea está lista. Para implementar una lista de versiones actualizable, necesitamos dos cosas: una lista de fuentes existente con números de versión válidos a priori y una variable de tipo de parámetro Choice en la tarea. En nuestro ejemplo, deje que la variable se llame BUILD_VERSION , no nos detendremos en ella en detalle. Pero echemos un vistazo más de cerca a la lista de fuentes.



No hay tantas opciones. Dos inmediatamente se me ocurrieron:



  • Utilice la API de acceso remoto que Jenkins ofrece a sus usuarios;
  • Consultar el contenido de la carpeta del repositorio remoto (en nuestro caso, este es JFrog Artifactory, que no es importante).


API de acceso remoto de Jenkins



De acuerdo con la fina tradición establecida, prefiero evitar largas explicaciones.

Solo me permitiré traducir libremente una parte del primer párrafo de la primera página de la documentación de la API :

Jenkins proporciona una API para acceso remoto legible por máquina a su funcionalidad. <...> El acceso remoto se ofrece en estilo REST. Esto significa que no hay un único punto de entrada para todas las capacidades, sino que se utiliza una URL como " ... / api / ", donde " ... " es el objeto al que se aplican las capacidades de la API.
En otras palabras, si la tarea de implementación, de la que estamos hablando en este momento, está disponible en la dirección http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, entonces los silbidos de API para esta tarea están disponibles en Siguiente, tenemos una opción en qué forma recibir la salida. Detengámonos en XML, ya que la API solo permite filtrar en este caso. Intentemos obtener una lista de todas las ejecuciones de trabajos. Solo nos interesa el nombre del ensamblado ( displayName ) y su resultado ( resultado ):http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/











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


¿Sucedió?



Ahora filtremos solo aquellos lanzamientos que terminan con un resultado EXITOSO . Usamos el argumento & exclude y le pasamos la ruta a un valor no igual a SUCCESS como parámetro . Sí Sí. La doble negación es una declaració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 exitosos




Bueno, solo por el bien de la autocomplacencia, asegurémonos de que el filtro no nos haya engañado (¡los filtros nunca mienten!) Y muestre una lista de los "fallidos":



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 fallidos




Lista de versiones de una carpeta en un servidor remoto



Existe una segunda forma de obtener una lista de versiones. Me gusta incluso más que la llamada a la API de Jenkins. Bueno, porque si la aplicación se compila con éxito, entonces ha sido empaquetada y colocada en el repositorio en la carpeta apropiada. Por ejemplo, el repositorio es el repositorio predeterminado de versiones de trabajo de aplicaciones. Me gusta. Bueno, preguntémosle qué versiones están almacenadas. Curl, grep y awk la carpeta remota. Si alguien está interesado en el unliner, entonces está debajo del spoiler.



Comando de una línea
: , , . :



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 trabajos y archivo de configuración de trabajos en Jenkins



Hemos tratado con la fuente de la lista de versiones. Atornillemos ahora la lista resultante en la tarea. Para mí, la solución obvia fue agregar un paso en el trabajo de compilación de la aplicación. El paso que se realizaría si el resultado fuera "correcto".



Abra la configuración de la tarea de ensamblaje y desplácese hasta el final. Haga clic en los botones: Agregar paso de compilación -> Paso condicional (único) . En la configuración del paso, seleccione la condición de estado de compilación actual , establezca el valor de ÉXITO , la acción a realizar si el comando Ejecutar shell tiene éxito .



Y ahora la parte divertida. Jenkins almacena configuraciones de trabajo en archivos. En formato XML. Por el caminohttp://--/config.xmlEn consecuencia, puede descargar el archivo de configuración, editarlo según sea necesario y colocarlo en el lugar de donde se tomó.



¿Recuerda que anteriormente acordamos que crearemos un parámetro BUILD_VERSION para la lista de versiones ?



Descarguemos el archivo de configuración y echemos un vistazo a su interior. Solo para asegurarse de que el parámetro esté en su lugar y sea realmente del tipo correcto.



Captura de pantalla debajo del spoiler.



Su fragmento config.xml debería verse igual. Excepto que el contenido del elemento de opciones aún no está presente




Estas convencido Muy bien, estamos escribiendo un script que se ejecutará en caso de una compilación exitosa.

El script recibirá una lista de versiones, descargará un archivo de configuración, escribirá una lista de versiones en el lugar que necesitemos y luego lo volverá a colocar. Si. Todo es correcto. Escribe una lista de versiones en XML en el lugar donde ya existe una lista de versiones (estará en el futuro, después del primer lanzamiento del script). Sé que todavía hay algunos amantes feroces de las expresiones regulares en el mundo. Yo no les pertenezco. Por favor instale xmlstarler en la máquina donde se va a editar la configuración. Me parece que este no es un precio muy alto por evitar la edición de XML con sed.



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



Escribimos en la configuración la lista de versiones de 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 '%s\n' "${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 te gustó más la opción de obtener versiones de Jenkins y eres tan vago como yo, entonces debajo del spoiler el mismo código, pero la lista es de Jenkins:



Escribimos una lista de versiones de Jenkins a la configuración
: , . , awk . .



#!/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 '%s\n' "${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 probó el código escrito sobre la base de los ejemplos anteriores, en la tarea de implementación ya debería tener una lista desplegable con las versiones. Aquí hay algo como la captura de pantalla debajo del spoiler.



Lista de versiones completada correctamente




Si todo funcionó, copie y pegue el script en el comando Ejecutar shell y guarde los cambios.



Conexión Cloud Shell



Los recolectores están en nuestros contenedores. Usamos Ansible como nuestro administrador de configuración y entrega de aplicaciones. En consecuencia, cuando se trata de crear 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. Hemos acordado guardar silencio sobre los complementos para Jenkins en este artículo. ¿Recuerda?



Decidí: bueno, dado que los contenedores "listos para usar" se pueden ensamblar en la consola en la nube, ¿por qué cercar un huerto? Mantenlo limpio, ¿verdad? Quiero crear contenedores con Jenkins en la consola en la nube y luego dispararlos a Kuber desde allí. Además, Google tiene canales muy ricos dentro de la infraestructura, lo que tendrá un efecto beneficioso en la velocidad de implementación.



Se necesitan dos cosas para conectarse a la consola en la nube: gcloudy derechos de acceso a la API de Google Cloud para la instancia de VM desde la que se realizará esta conexión.



Para aquellos que planean conectarse no desde la nube de Google en absoluto
. , *nix' .



, — . — .



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



  1. Detenga la instancia de VM desde la que se conectará a la consola en la nube en el futuro.
  2. Abra Detalles de instancia y haga clic en Editar .
  3. En la parte inferior de la página, seleccione el alcance de acceso a la instancia Acceso completo a todas las API de la nube .



    Captura de pantalla


  4. Guarde sus cambios e inicie la instancia.


Una vez que la VM haya terminado de iniciarse, conéctese a través de SSH y asegúrese de que la conexión sea exitosa. Usa el comando:



gcloud alpha cloud-shell ssh


Una conexión exitosa se parece a esto




Implementar en GKE



Dado que nos esforzamos en todas las formas posibles para cambiar completamente a IaC (Infraestructura como código), almacenamos archivos docker en el gita. Esto es por un lado. Una implementación en kubernetes se describe mediante un archivo yaml que solo usa esta tarea, que en sí misma también es como un código. Esto está del otro lado. En general, quiero decir que el plan es el siguiente:



  1. Tomamos los valores de las variables BUILD_VERSION y, opcionalmente, los valores de las variables que pasarán por ENV .
  2. Descargando dockerfile desde el gita.
  3. Generando yaml para implementación.
  4. Sube ambos archivos a través de scp a la consola en la nube.
  5. Cree un contenedor allí y envíelo al registro de contenedores
  6. Aplicamos el archivo de implementación de carga a Kuber.


Seamos más específicos. Desde que empezamos a hablar de ENV , supongamos que necesitamos pasar los valores de dos parámetros: PARAM1 y PARAM2 . Agregue su tarea para la implementación, escriba - String Parameter .



Captura de pantalla




Generaremos yaml simplemente redirigiendo echo a un archivo. Se asume, por supuesto, que tiene PARAM1 y PARAM2 en el dockerfile , que el nombre de la carga será awesomeapp , y que el contenedor ensamblado con la aplicación de la versión especificada está en el registro del contenedor a lo largo de la ruta gcr.io/awesomeapp/awesomeapp- $ BUILD_VERSION , donde $ BUILD_VERSION es 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




Después de conectarse mediante gcloud alpha cloud-shell ssh al agente de Jenkins, el modo interactivo no está disponible, por lo que enviamos comandos a la consola en la nube mediante el parámetro --command .



Limpiamos la carpeta de inicio en la consola en la nube del antiguo archivo docker:



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


Colocamos el dockerfile recién descargado en la carpeta de inicio de la consola en la nube usando scp:



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


Recopilamos, etiquetamos y enviamos el contenedor al registro de contenedores:



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"


Hacemos lo mismo con el archivo de implementación. Tenga en cuenta que los siguientes comandos utilizan nombres ficticios para el clúster donde se lleva a cabo la implementación ( 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"


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



Captura de pantalla




Y luego el despliegue exitoso del contenedor ensamblado



Captura de pantalla




He ignorado deliberadamente la configuración de Ingress . Por una sencilla razón: una vez que lo haya configurado para una carga de trabajo con un nombre dado, seguirá operativo, sin importar cuántas implementaciones con este nombre se realicen. Bueno, en general, esto está un poco más allá del alcance de la historia.



En lugar de conclusiones



Todos los pasos anteriores, probablemente, no se podrían haber realizado, pero simplemente instalaron algún complemento para Jenkins, su muuulion. Pero de alguna manera no me gustan los complementos. Bueno, más precisamente, recurro a ellos solo por desesperación.



Y solo me gusta recoger algún tema nuevo para mí. El texto anterior también es una forma de compartir los hallazgos que hice, resolviendo el problema descrito al principio. Comparte con aquellos que, como, no son en absoluto un lobo terrible en devops. Si mis hallazgos ayudan al menos a alguien, seré feliz.



All Articles