Pipeline en Jenkins para Rollback en Función del Entorno
La entrada de hoy es algo de lo que me siento realmente orgullosa. Llevo pocos meses trabajando con Jenkins y familiarizándome con su lenguaje, y hacer un script que te permita el rollback es una herramienta que pienso guardar en mi cajón de sastre.
Las necesidades que tenía eran:
- Elegir la imagen del container registry para un deployment y un container dado de Kubernetes, en función del entornos, en mi caso prod y dev.
- Necesitaba que me generara una lista de las etiquetas de la imagen del Container Registry de GCP en función del entorno.
- Necesitaba que fuera interactivo, y que el usuario que lo lanza pudiera decidir que versión restaurar.
En mi caso, he elegido el objeto de Jenkins pipeline, que me da un poco más de manga ancha para «programar». Y este es el resultado:
pipeline {
agent any
environment {
ENVIRONMENT = ""
IMAGE = gcr.io/[PROJECT_ID]/[IMAGE_NAME]
CMD=""
TAGS=""
K8_OBJ="deployment/[deployment-name]"
CONTAINER="[container_name]"
}
stages {
stage("Select Environment") {
steps {
script {
// Variables for input
// Get the environment
def envInput = input(
id: 'envInput', message: 'Enter path of test reports:?',
parameters: [
choice(name: 'ENVIRONMENT',
choices: ['prod','dev'].join('\n'),
description: 'Please select the Environment')
])
// Save to variables. Default to empty string if not found.
ENVIRONMENT = envInput?:''
}
}
}
stage("Select IMAGE f(x) env") {
steps {
script {
if (ENVIRONMENT == 'prod') {
//Image si prod
IMAGE = "gcr.io/[PROJECT_ID]/[IMAGE_NAME]"
} else {
//Image si dev
IMAGE = "gcr.io/[PROJECT_ID]/[IMAGE_NAME]"
}
}
}
}
stage("Select available tag") {
steps {
script {
//Generar la lista de etiquetas disponibles para la imagen dada
CMD="gcloud container images list-tags $IMAGE | awk 'NR==2,NR==12' | awk '{print \$2}' | awk -F, '{print \$1}'"
TAGS=sh (returnStdout: true, script: CMD ).trim()
//Recoger la etiqueta seleccionada por el usuario
def tagInput = input(
id: 'tagInput', message: 'Enter path of test reports:?',
parameters: [
choice(name: 'TAGS',
choices: [TAGS].join('\n'),
description: 'Please select the Environment')
])
//Guardar la etiqueta seleccionada por el usuario.
TAG = tagInput?:''
}
}
}
stage("Rollback To Selected Version"){
steps {
sh "kubectl set image ${K8_OBJ} ${CONTAINER}=${IMAGE}:${TAG} --record -n ${ENVIRONMENT}"
}
}
}
}
Y ya estaría. Problema resuelto!
Configurar SnipeIT como deployment en Kubernetes sobre Google Cloud Platform
La nota mental de hoy va sobre una herramienta OpenSource para la gestión de inventario que estamos probando en la oficina: SnipeIT. La manera de documentar como realizar una instalación, ya fuera sobre VM o sobre docker me pareció realmente confusa.
Invertí bastantes horas en entender los ficheros de configuración. Cosas tan sencillas como especificar que el docker-compose up debía ir con el atributo -d, para poder seguir utilizando la consola, no estaban especificadas. Al igual que el orden para realizar los pasos.
A continuación, os detallo los pasos que seguí para poder llevar a cabo el despligue como deployment en el entorno de Kubernetes de GCP.
Introducción
Snipe-IT permite una gestión fácil para 4 tipos principales de activos:
- Equipos/Terminales
- Licencias
- Accesorios
- Consumibles
Permite tener una traza de quién tiene qué portátil/pc, cuándo se ha comprado, dónde, qué licencias de software y accesorios están disponibles, etc.
Es un software con solamente interfaz web, y alguna de las cosas que más me han gustado es la capacidad de vincular los usuarios con un LDAP o AD. Está basado en el framework Laravel, y el fichero de configuración es el estándar del mismo.
Snipe-IT requiere de una conexión a base de datos para almacenar el contenido. Es compatible con varios tipos diferentes de bases de datos, pero en esta nota mental, trabajaremos con MySQL 5.6
Ojo: Para poder lanzar correctamente la aplicación en Kubernetes es necesario generar una key.
Instalación en local
En la documentación de SnipeIT no hay un apartado para Kubernetes, así que lo que tuve que hacer es adaptar los archivos que ellos facilitaban.
Estos son los archivos para poder lanzar snipe-it como contenedor local.
docker-compose.yml
version: '3'
services:
snipe-mysql:
container_name: snipe-mysql
image: mysql:5.6
env_file:
- ./.env
volumes:
- snipesql-vol:/var/lib/mysql
command: --default-authentication-plugin=mysql_native_password
expose:
- "3306"
snipe-it:
image: snipe/snipe-it
env_file:
- ./.env
ports:
- "80:80"
depends_on:
- snipe-mysql
volumes:
snipesql-vol:
SnipeIT corre un Apache de manera interna. En este caso he mapeado el 80 de la aplicación al 80 de mi máquina, para simplificarlo todo.
Por otra parte, este es el fichero de variables de entorno oficial:
.env
# Mysql Parameters
MYSQL_PORT_3306_TCP_ADDR=snipe-mysql
MYSQL_ROOT_PASSWORD=YOUR_SUPER_SECRET_PASSWORD
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=YOUR_snipeit_USER_PASSWORD
# Email Parameters
# - the hostname/IP address of your mailserver
MAIL_PORT_587_TCP_ADDR=smtp.whatever.com
#the port for the mailserver (probably 587, could be another)
MAIL_PORT_587_TCP_PORT=587
# the default from address, and from name for emails
[email protected]
MAIL_ENV_FROM_NAME=Your Full Email Name
# - pick 'tls' for SMTP-over-SSL, 'tcp' for unencrypted
MAIL_ENV_ENCRYPTION=tcp
# SMTP username and password
MAIL_ENV_USERNAME=your_email_username
MAIL_ENV_PASSWORD=your_email_password
# Snipe-IT Settings
APP_ENV=production
APP_DEBUG=false
APP_KEY=<<Fill in Later!>>
APP_URL=http://127.0.0.1:80
APP_TIMEZONE=US/Pacific
APP_LOCALE=en
Para lanzarlo en local, situaremos nuestra consola en la carpeta donde hayamos generado estos dos archivos anteriores. A continuación, ejecutaremos el siguiente comando:
docker-compose up -d
El -d lo hará correr en segundo plano y podremos seguir trabajando con el mismo terminal
Generar la APP_KEY
Tenemos que acceder al bash del contenedor de snipe-it, para ello:
docker exec -it nombre-del-contenedor-snipe-it sh
Y ejecutamos el siguiente comando:
php artisan key:generate
Nos debería devolver un texto tal que:
**************************************
* Application In Production! *
**************************************
Do you really wish to run this command? (yes/no) [no]:
Escribimos yes y pulsamos Enter. Debería devolver algo similar a:
Application key [base64:mW05bo4UXv6D/t3ldTzjUvIbUkwyKdrPSVlr/mrE3Ac=] set successfully.
La key en este caso sería:
base64:mW05bo4UXv6D/t3ldTzjUvIbUkwyKdrPSVlr/mrE3Ac=
Es importante no olvidar el base64, puesto que sino, la aplicación no funcionará correctamente.
Archivos de configuración en Kubernetes
Necesitamos:
- Disco de almacenamiento persistente (PVC)
- Configmap para guardar las variables de sistema
- Secrets para guardar las variables sensibles y contraseñas
- Servicios, uno para mysql y otro para snipe-it.
- Deployment
01-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: snipeit-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
02-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: snipe-it-config
data:
# Mysql Parameters
MYSQL_PORT_3306_TCP_ADDR: "mysql-service"
MYSQL_PORT_3306_TCP_PORT: "3306"
MYSQL_DATABASE: "snipeit"
MYSQL_USER: "snipeit"
# Email Parameters
# - the hostname/IP address of your mailserver
MAIL_PORT_587_TCP_ADDR: smtp.whatever.com
#the port for the mailserver (probably 587, could be another)
MAIL_PORT_587_TCP_PORT: 587
# the default from address, and from name for emails
MAIL_ENV_FROM_ADDR: [email protected]
MAIL_ENV_FROM_NAME: Your Full Email Name
# - pick 'tls' for SMTP-over-SSL, 'tcp' for unencrypted
MAIL_ENV_ENCRYPTION: tcp
# SMTP username and password
MAIL_ENV_USERNAME: your_email_username
MAIL_ENV_PASSWORD: your_email_password
# Snipe-IT Settings
APP_ENV: "production"
APP_DEBUG: "false"
APP_KEY: "base64:mW05bo4UXv6D/t3ldTzjUvIbUkwyKdrPSVlr/mrE3Ac="
APP_URL: "http://0.0.0.0:80"
APP_TIMEZONE: "Europe/Madrid"
APP_LOCALE: "es-ES"
Dónde:
- MYSQL_PORT_3306_TCP_ADDR: «mysql-service» corresponde al servicio de de MySQL que crearemos en archivos posteriores.
- MYSQL_PORT_3306_TCP_PORT: «3306» es el valor por defecto del puerto de MySQL
- MYSQL_DATABASE: «snipeit» es el nombre por defecto de la base de datos
- MYSQL_USER: «snipeit» es el usuario por defecto de la base de datos.
- APP_KEY: Es la clave que hemos generado previamente en local.
03-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: snipe-it-secret
type: Opaque
data:
MYSQL_ROOT_PASSWORD: "tu-contraseña-root-mysql-en-base-64"
MYSQL_PASSWORD: "tu-contraseña-root-mysql-en-base-64"
04-mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql-service
labels:
app: snipeit
spec:
ports:
- port: 3306
protocol: TCP
targetPort: 3306
selector:
app: snipeit
sessionAffinity: None
type: LoadBalancer
status:
loadBalancer: {}
En cuanto hablamos de servicios, no hay que perder de vista las etiquetas. Son las que nos permitirán asociar pods y deployments a servicios. En mi caso utilizo la etiqueta «app» para realizar posteriormente la concordancia en el deployment.
En mi caso he elegido desplegar tanto el servicio de MySQL como el de Snipe-IT como balanceador de carga, lo cual me generará una IP pública accesible. Podría hacerse también usando IP de Clúster y un Ingress, entre otras opciones.
05-snipeit-service
apiVersion: v1
kind: Service
metadata:
name: snipeit-service
labels:
app: snipeit
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: snipeit
sessionAffinity: None
type: LoadBalancer
status:
loadBalancer: {}
06-deployment.yaml
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
name: "snipeit-deployment"
labels:
app: "snipeit"
spec:
replicas: 1
selector:
matchLabels:
app: snipeit
template:
metadata:
labels:
app: snipeit
spec:
containers:
### mysql image ###
- name: snipe-mysql
image: mysql:5.6
ports:
- containerPort: 3306
envFrom:
- configMapRef:
name: snipe-it-config
- secretRef:
name: snipe-it-secret
volumeMounts:
- name: snipeit-vol
mountPath: /var/lib/mysql
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "sleep 60"]
### snipe it image ###
- name: snipeit
image: snipe/snipe-it
envFrom:
- configMapRef:
name: snipe-it-config
- secretRef:
name: snipe-it-secret
ports:
- containerPort: 80
volumeMounts:
- name: snipeit-vol
mountPath: /var/lib/snipeit
#volumes of the pod
volumes:
- name: snipeit-vol
persistentVolumeClaim:
claimName: snipeit-pvc
Dónde:
- snipe-it-config: Es el nombre asignado al fichero de ConfigMap
- snipe-it-secret: Es el nombre asignado al fichero de Secrets.
- snipeit-pvc: Es el nombre asignado al disco persistente creado.
- snipeit-vol: Es el nombre asignado al volumen para utilizar el pvc dentro de la plantilla del depliegue.
Recuerda: Has de tener previamente instalada la herramienta Google SDK y el plugin kubectl.
Para finalizar, desde la consola de SDK, nos situamos en la carpeta donde hayamos generado los ficheros de configuración y lanzamos.
kubectl create -f . --save-config
No te olvides de probar que todo funciona como se espera. En este caso, bastaría con poner en un navegador la IP pública que haya asignado Google para el balanceador de carga.
Os dejo la documentación oficial en la que me he basado, por si os es de utilidad.
Problema resuelto!
Ejecutar comandos en una imagen docker contenida en un pod tras su arranque
Cuanto más conozco de Kubernetes, GCP y docker, más consciente soy de que es todo un universo paralelo en continuo cambio. Universo, porque a nivel de posibilidades, flexibilidad, configuraciones… las opciones son muy diversas pero no por ello excluyentes. La problemática que se me planteaba era la siguiente:
Tenía un despliegue (deployment), que generaba a partir de un yaml sencillo, el cual contenía básicamente un par de imágenes y los puertos expuestos. En una de esas imágenes, había que crear por seguridad un nuevo usuario con sus respectivos permisos y accesos.
En casos normales, hubiera bastado con levantar el deployment y asignarle almacenamiento persistente. El problema era que la configuración de este servicio se guardaba vinculada al nombre del pod. La situación era tal que así:
config@pod1
config@pod2
Mientras el pod estuviera vivo, no había problema, pero si el pod moría y se levantaba otro distinto, se perdía esa configuración. Y seamos realistas, Kubernetes está diseñado para que los pods se mueran en cuanto dejan de funcionar como se espera.
Solución: Crear ese usuario a la vez que el pod, ejecutando comandos de consola específicos. Así, cuando se levantase el pod con el nombre que se levantase, ese usuario existiría y funcionaría.
Ejemplo de lanzamiento de comando tras la creación del contenedor
apiVersion: v1
kind: Deployment
metadata:
...
spec:
replicas: 1
template:
metadata:
...
spec:
containers:
- name: auth
image: [imagen-del-servicio]
env:
ports:
- containerPort: 3000
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "[cmd]"]
Donde:
- [imagen-del-servicio] es el nombre de la imagen que estamos utilizando para crear el contenedor dentro del pod.
- [cmd] es el comando/s que queremos lanzar.
- command: Sus corchetes son obligatorios.
A grandes rasgos, la configuración para lanzar los comandos viene definida dentro de la etiqueta lifecycle. Para más info, podéis consultar la documentación oficial de Kubernetes sobre cómo adjuntar controladores a eventos de ciclo de vida del contenedor.
Ojo! Es posible que la imagen de aplicación que estáis desplegando tarde unos segundos antes de poder aceptar comandos. Recomiendo siempre usar un sleep antes de lanzar ningún otro comando, donde [TIEMPO] es el valor en segundos que queremos darle.
command: ["/bin/sh", "-c", "sleep [TIEMPO]"]
Los comandos se pueden concatenar de la misma manera que lo haríamos en consola. Por ejemplo:
command: ["/bin/sh", "-c", "sleep 60 && echo 'Hola Mundo'"]
Problema resuelto!