Notas Mentales de Un SysAdmin

blog sobre tecnologías para sysadmin y devops

google cloud platform

Programar encendido/apagado de VM en GCE

Google Cloud tiene una solución para cada necesidad. Entre las muchas cosas que se pueden hacer, os hoy dejo un mini tutorial de como hacer que las instancias de VM se enciendan y se paren según la programación que nosotros le definamos.

1. Requisitos previos

  • Instancia/s de VM. Puedes ver aquí como crear una nueva instancia de VM.
  • Etiquetas en esa instancia/s.

Para esta documentación, utilizaremos la etiqueta schedule:l-v.

2. Crear Cloud Functions con Cloud Pub/Sub

2.1. Crear la función de inicio.

  1. Ve a la página de Cloud Functions en GCP Console.
    Ir a la página de Cloud Functions
  2. Haz clic en Crear función.
  3. Configura el Nombre como startInstancePubSub.
  4. Deja el valor predeterminado en Memoria asignada.
  5. En Activador, selecciona Cloud Pub/Sub.
  6. En Tema, selecciona Create new topic…
  7. Aparecerá un cuadro de diálogo Nuevo tema pub/sub.
    1. En Nombre, ingresa start-instance-event.
    2. Haz clic en Crear para finalizar el cuadro de diálogo.
  8. En Entorno de ejecución, selecciona Node.js 10.
  9. Sobre el bloque de texto del código, selecciona la pestaña index.js.
  10. Reemplaza el código de inicio con lo siguiente:functions/scheduleinstance/index.js
const Compute = require('@google-cloud/compute');
const compute = new Compute();

exports.startInstancePubSub = async (event, context, callback) => {
  try {
    const payload = _validatePayload(
      JSON.parse(Buffer.from(event.data, 'base64').toString())
    );
    const options = {filter: `labels.${payload.label}`};
    const [vms] = await compute.getVMs(options);
    await Promise.all(
      vms.map(async instance => {
        if (payload.zone === instance.zone.id) {
          const [operation] = await compute
            .zone(payload.zone)
            .vm(instance.name)
            .start();

          // Operation pending
          return operation.promise();
        }
      })
    );
    const message = `Successfully started instance(s)`;
    console.log(message);
    callback(null, message);
  } catch (err) {
    console.log(err);
    callback(err);
  }
};
const _validatePayload = payload => {
  if (!payload.zone) {
    throw new Error(`Attribute 'zone' missing from payload`);
  } else if (!payload.label) {
    throw new Error(`Attribute 'label' missing from payload`);
  }
  return payload;
};

11. Sobre el bloque de texto del código, selecciona la pestaña package.json.
12. Reemplaza el código de inicio con lo siguiente:functions/scheduleinstance/package.json

{
  "name": "cloud-functions-schedule-instance",
  "version": "0.1.0",
  "private": true,
  "license": "Apache-2.0",
  "author": "Google Inc.",
  "repository": {
    "type": "git",
    "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
  },
  "engines": {
    "node": ">=8.0.0"
  },
  "scripts": {
    "test": "mocha test/*.test.js --timeout=20000"
  },
  "devDependencies": {
    "@google-cloud/nodejs-repo-tools": "^3.3.0",
    "mocha": "^6.0.0",
    "proxyquire": "^2.0.0",
    "sinon": "^7.0.0"
  },
  "dependencies": {
    "@google-cloud/compute": "^1.0.0"
  }
}

En Función a ejecutar, ingresa startInstancePubSub.

Haz clic en Crear.

2.2. Crear la función de detención

  1. Debes estar en la página de Cloud Functions en GCP Console.
  2. Haz clic en Crear función.
  3. Configura el Nombre como stopInstancePubSub.
  4. Deja el valor predeterminado en Memoria asignada.
  5. En Activador, selecciona Cloud Pub/Sub.
  6. En Tema, selecciona Create new topic…
  7. Aparecerá un cuadro de diálogo Nuevo tema pub/sub.
    1. En Nombre, ingresa stop-instance-event.
    2. Haz clic en Crear para finalizar el cuadro de diálogo.
  8. En Entorno de ejecución, selecciona Node.js 10.
  9. Sobre el bloque de texto del código, selecciona la pestaña index.js.
  10. Reemplaza el código de inicio con lo siguiente: functions/scheduleinstance/index.js
const Compute = require('@google-cloud/compute');
const compute = new Compute();

exports.stopInstancePubSub = async (event, context, callback) => {
  try {
    const payload = _validatePayload(
      JSON.parse(Buffer.from(event.data, 'base64').toString())
    );
    const options = {filter: `labels.${payload.label}`};
    const [vms] = await compute.getVMs(options);
    await Promise.all(
      vms.map(async instance => {
        if (payload.zone === instance.zone.id) {
          const [operation] = await compute
            .zone(payload.zone)
            .vm(instance.name)
            .stop();

          // Operation pending
          return operation.promise();
        } else {
          return Promise.resolve();
        }
      })
    );
    const message = `Successfully stopped instance(s)`;
    console.log(message);
    callback(null, message);
  } catch (err) {
    console.log(err);
    callback(err);
  }
};

const _validatePayload = payload => {
  if (!payload.zone) {
    throw new Error(`Attribute 'zone' missing from payload`);
  } else if (!payload.label) {
    throw new Error(`Attribute 'label' missing from payload`);
  }
  return payload;
};

11. Sobre el bloque de texto del código, selecciona la pestaña package.json.
12. Reemplaza el código de inicio con lo siguiente: functions/scheduleinstance/package.json

{
  "name": "cloud-functions-schedule-instance",
  "version": "0.1.0",
  "private": true,
  "license": "Apache-2.0",
  "author": "Google Inc.",
  "repository": {
    "type": "git",
    "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
  },
  "engines": {
    "node": ">=8.0.0"
  },
  "scripts": {
    "test": "mocha test/*.test.js --timeout=20000"
  },
  "devDependencies": {
    "@google-cloud/nodejs-repo-tools": "^3.3.0",
    "mocha": "^6.0.0",
    "proxyquire": "^2.0.0",
    "sinon": "^7.0.0"
  },
  "dependencies": {
    "@google-cloud/compute": "^1.0.0"
  }
}

13. En Función a ejecutar, ingresa stopInstancePubSub.
14. Haz clic en Crear.

3. Verifica que tus funciones actúen correctamente

3.1. Detén la instancia

  1. Ve a la página de Cloud Functions en GCP Console.
    Ir a la página de Cloud Functions
  2. Haz clic en la función denominada stopInstancePubSub.
  3. Deberías ver algunas pestañas: GeneralActivadorFuente, y Prueba. Haz clic en la pestaña Prueba.
  4. Para Evento de activación, ingresa lo siguiente:1 {"data":"eyJ6b25lIjoiZXVyb3BlLXdlc3QxLWIiLCJsYWJlbCI6InNjaGVkdWxlOmwtdiJ9"} 
    • Esto es simplemente una string codificada en base64 que contiene la siguiente información: {«zone»:»europe-west1-b»,»label»:»schedule:l-v»}
    • Si deseas codificar tu propia string, puedes usar cualquier herramienta de codificación en base64 en línea.
  5. Haz clic en el botón Probar la función.
  6. Cuando haya terminado de ejecutarse, deberías ver el texto Successfully stopped instance dev-app-01-instance debajo de Resultado. La ejecución puede tardar hasta 60 segundos en completarse.
  7. Ve a la página Instancias de VM en GCP Console.
    Ir a la página Instancias de VM
  8. Verifica que la instancia denominada dev-app-01 tenga un recuadro gris junto a su nombre. Esto indica que se detuvo. Puede tardar hasta 30 segundos en terminar de desactivarse.
    • Si parece que no va a finalizar, prueba hacer clic en Actualizar en la parte superior de la página.

3.2. Inicia la instancia

  1. Ve a la página de Cloud Functions en GCP Console.
    Ir a la página de Cloud Functions
  2. Haz clic en la función denominada startInstancePubSub.
  3. Deberías ver algunas pestañas: GeneralActivadorFuente, y Prueba. Haz clic en la pestaña Prueba.
  4. Para Evento de activación, ingresa lo siguiente:1 {"data":"eyJ6b25lIjoiZXVyb3BlLXdlc3QxLWIiLCJsYWJlbCI6InNjaGVkdWxlOmwtdiJ9"} 
    • Nuevamente, esto es simplemente la string codificada en base64 para {«zone»:»europe-west1-b»,»label»:»schedule:l-v»}
  5. Haz clic en el botón Probar la función.
  6. Cuando haya terminado de ejecutarse, deberías ver el texto Successfully started instance workday-instance debajo de Resultado.
  7. Ve a la página Instancias de VM en GCP Console.
    Ir a la página Instancias de VM
  8. Verifica que la instancia denominada dev-app-01 tenga una marca de verificación verde junto a su nombre. Esto indica que se está ejecutando. Puede tardar hasta 30 segundos en terminar de iniciarse.

4. Configurar los trabajos de Cloud Scheduler para llamar a Cloud Pub/Sub

Nota: La programación se especifica con el formato cron para UNIX. Ejemplos: Cada minuto «* * * * *»; cada 3 horas «0 */3 * * *»; todos los lunes a las 9:00 «0 9 * * 1». Más información

4.1. Crea el trabajo de inicio

  1. Ve a la página de Cloud Scheduler en GCP Console.
    Ir a la página de Cloud Scheduler
  2. Haz clic en Crear trabajo.
  3. Configura el Nombre como startup-l-v-instance.
  4. En Frecuencia, ingresa 0 6 * * 1-5.
  5. En Zona horaria, selecciona el país y la zona horaria que desees. En este ejemplo, usaremos United States y Los Angeles.
  6. En Destino, selecciona Pub/Sub.
  7. En Tema, ingresa start-instance-event.
  8. En Carga útil, ingresa lo siguiente:1 {"zone":"europe-west1-b","label":"schedule:l-v"}
  9. Haz clic en Crear.

4.2. Crea el trabajo de detención.

  1. Debes estar en la página de Cloud Functions en GCP Console.
  2. Haz clic en Crear trabajo.
  3. Configura el Nombre como shutdown-l-v-instance.
  4. En Frecuencia, ingresa 0 21 * * 1-5.
  5. En Zona horaria, selecciona el país y la zona horaria que desees. En este ejemplo, usaremos United States y Los Angeles.
  6. En Destino, selecciona Pub/Sub.
  7. En Tema, ingresa stop-instance-event.
  8. En Carga útil, ingresa lo siguiente:1 {"zone":"europe-west1-b","label":"schedule:l-v"}
  9. Haz clic en Crear.

5. Verifica que los trabajos funcionen

5.1. Detén la instancia

  1. Ve a la página de Cloud Scheduler en GCP Console.
    Ir a la página de Cloud Scheduler
  2. En el trabajo denominado shutdown-l-v-instance, haz clic en el botón Ejecutar ahora en el extremo derecho de la página.
  3. Ve a la página Instancias de VM en GCP Console.
    Ir a la página Instancias de VM
  4. Verifica que la instancia denominada app-dev-01-instance tenga un recuadro gris junto a su nombre. Esto indica que se detuvo. Puede tomar hasta 30 segundos para que termine de desactivarse.

5.2. Inicia la instancia

  1. Ve a la página de Cloud Scheduler en GCP Console.
    Ir a la página de Cloud Scheduler
  2. En el trabajo denominado startup-l-v-instance, haz clic en el botón Ejecutar ahora en el extremo derecho de la página.
  3. Ve a la página Instancias de VM en GCP Console.
    Ir a la página Instancias de VM
  4. Verifica que la instancia denominada dev-app-01-instance tenga una marca de verificación verde junto a su nombre. Esto indica que se está ejecutando. Puede tardar hasta 30 segundos para que termine de iniciarse.

Problema resuelto!

Subir imágenes al Container Registry de GCP

No es nada complicado, pero es importante que hay tres maneras diferentes de subir una imágen al repositorio de GCP.

Dockerfile

1. Crea o descarga un fichero Dockerfile

Con tu editor de texto favorito, o bien descargado de Dockerhub, sitúa el fichero Dockerfile en la carpeta que quieras.

2. Compila la imagen con Dockerfile

Cloud Build te permite compilar una imagen de Docker mediante un Dockerfile. No necesitas un archivo de configuración de compilación diferente.

Ejecuta el comando siguiente desde el directorio que contiene quickstart.sh y Dockerfile, en el que [PROJECT_ID] es tu ID del proyecto de GCP:

gcloud builds submit --tag gcr.io/[PROJECT_ID]/[IMAGE_NAME] .

Ejemplo:

gcloud builds submit --tag gcr.io/tg-gcp-project/rabbitmq .

YAML

  1. Crea un Dockerfile con la información que necesites.
  2. En el mismo directorio que contiene Dockerfile, crea un archivo llamado cloudbuild.yaml con los contenidos siguientes. Este archivo es tu archivo de configuración de compilación. A la hora de la compilación, Cloud Build reemplaza $PROJECT_ID con tu ID del proyecto de manera automática.
steps:
  name: 'gcr.io/cloud-builders/docker'
  args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/rabbitmq2', '.' ]
images: 'gcr.io/$PROJECT_ID/rabbitmq2'

3. Comienza la compilación con la ejecución del comando siguiente:1 gcloud builds submit --config cloudbuild.yaml .

Nota

No omitas el «.» al final del comando anterior. Con “.”, se especifica que el código fuente se encuentra en el directorio de trabajo actual al momento de la compilación.

Imagen Local

Etiqueta la imagen local con el nombre del registro mediante el siguiente comando:

docker tag [SOURCE_IMAGE] [HOSTNAME]/[PROJECT-ID]/[IMAGE]

Envía la imagen etiquetada a Container Registry con el siguiente comando:

docker push [HOSTNAME]/[PROJECT-ID]/[IMAGE]
docker push [HOSTNAME]/[PROJECT-ID]/[IMAGE]:[TAG]

Problema resuelto!

Chuleta de YAML para Kubernetes en GCP

Nunca viene mal una chuleta cuando se trabaja con Kubernetes. En este caso, es una chuleta especial para el entono de GKE en la plataforma de GCP. En este caso, son el tipo de cargas de trabajo, servicios, etc. que más suelo usar en mi día a día. Espero que os sean de utilidad.

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: name-deployment
  namespace: dev
  labels:
    app: app-name
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-name
  template:
    metadata:
      labels:
        app: app-name
    spec:
      containers:
      - name: container-name
        image: container-image:tag
        ports:
          - containerPort: 1234
        envFrom:
          - configMapRef:
              name: configmap-name
          - secretRef:
              name: secret-name
        volumeMounts:
          - name: name-vol
            mountPath: /unix/source/route/map
      volumes:
        - name: name-vol
          persistentVolumeClaim:
            claimName: name-pvc

StatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: name-statefulset
  namespace: dev
  labels:
    app: app-name
spec:
  serviceName: app-service-name"
  replicas: 1
  selector:
    matchLabels:
      app: app-name
  template:
    metadata:
      labels:
        app: app-name
    spec:
      containers:
        - name: container-name
          image: container-image:tag
          ports:
            - containerPort: 1234
              name: port-name
          resources:
            limits:
              memory: "2000Mi"
            requests:
              memory: "300Mi"
          envFrom:
          - configMapRef:
              name: configmap-name
          - secretRef:
              name: secret-name
        volumeMounts:
          - name: name-vol
            mountPath: /unix/source/route/map
      volumes:
        - name: name-vol
          persistentVolumeClaim:
            claimName: name-pvc

Cronjob

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: name-cronjob
  namespace: dev
spec:
  schedule: "01 0 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: container-name
              image: container-image:tag
              args:
                - /bin/sh
                - -c
                - sh /scripts/script.sh
          restartPolicy: Never

PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: name-pvc
  namespace: dev
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 25Gi

ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: genportal-integration-config
  namespace: dev
data:
  #Comments
  VAR_NAME: "value"

Secrets

apiVersion: v1
kind: Secret
metadata:
  name: name-secret
  namespace: dev
type: Opaque
data:
  VAR_NAME: "password_in_base_64"

Service

apiVersion: v1
kind: Service
metadata:
  name: name-service
  labels:
    app: app-name
  namespace: dev
spec:
  ports:
    - port: 1234
      protocol: TCP
      targetPort: 1234
  selector:
    app: app-name
  sessionAffinity: None
  type: LoadBalancer / ClusterIP / NodePort
status:
  loadBalancer: {}

Tipos de servicio en función de su comportamiento:

  • ClusterIP: expone el servicio en una IP interna del clúster. Elegir este valor hace que el Servicio solo sea accesible desde dentro del clúster. Este es el ServiceType predeterminado.
  • NodePort: expone el servicio en la IP de cada nodo en un puerto estático (el NodePort). Se crea automáticamente un servicio ClusterIP, al que se enruta el servicio NodePort. Podrá ponerse en contacto con el servicio NodePort, desde fuera del clúster, solicitando <NodeIP>: <NodePort>.
  • LoadBalancer: expone el servicio de forma externa mediante el equilibrador de carga de un proveedor de nube. Los servicios NodePort y ClusterIP, a los que se enruta el equilibrador de carga externo, se crean automáticamente.

Problema resuelto!

Snapshots periódicos de un PVC en GCP con Google SDK + Bash + CronJob

Pues ahí va la historia de este script:

Resulta que queremos mover una base de datos, la típica postgresql, de una maquina virtual a un pod de kubernetes. Hay muchos factores a tener en cuenta: Si se va a hacer subida de versión, como importar una copia de la base de datos, persistencia de discos… Aunque el que más me preocupaba era el disaster recovery.

Con una VM era sencillo. Script sacado de un blog de google y adaptado al entorno GCP, cambiando el almacenamiento local por un bucket de Storage. Pero claro, la mayoría de imágenes de postgresql vienen sobre alpine, que viene con las funcionalidades más que justas para que su consumo sea mínimo.

Se me ocurrieron un par de soluciones para abordar el problema:

  • Instalar cron en el archivo Dockerfile de la imagen. Importar también los scripts actuales de la VM a dicha imágen. En definitiva, emular la VM en el entorno GKE.
  • Levantar una imagen google/cloud-sdk, copiar un script .sh que trabajara con snapshots de Kubernetes y ejecutarlo desde un CronJob.

Al final, la que me pareció más adecuada fue la segunda. Y es que hace poco, intentando implementar ELK, descubrí «nuevos» tipos de carga de trabajo más allá de los deployments: Los CronJobs.

Un CronJob es un tipo de carga de trabajo que permite ejecutar trabajos en una programación basada en el tiempo. Estos trabajos automatizados se ejecutan como tareas Cron en un sistema Linux o UNIX.

Este tipo de carga de trabajo, junto a la potencia del SDK de Google, que permite trabajar con cuentas de servicio de permisos limitados, hacen posible que este script funcione. Eh aquí mis archivos «mágicos»:

Dockerfile

FROM google/cloud-sdk:latest

RUN mkdir /scripts

COPY ./[service-account-key].json /scripts
COPY ./snapshot_creation.sh /scripts

RUN chmod +x /scripts/snapshot_creation.sh

CMD tail -f /dev/null

Para poder utilizar GKE de forma desatendida, es necesario utilizar una cuenta de servicio. Os dejó aquí un enlace de como activar y empezar a usar una cuenta de servicio con GCP.

Script en Bash

#VAR definition
account_service_email="[account-service-email]"
account_service_json_dir="/scripts/.[service-account-key].json"
cluster_name="[cluster-name]"
cluster_zone="[cluster-zone]"
date=$(date +%Y%m%d-%H%M)
gcp_project_name="[gcp-project-name]"
pvc_name="pvc-[id]"
snapshot_name=$date-prod-psql-snapshot
snapshot_namespace="[kubernetes-namespace]"


#Create YAML
printf "apiVersion: snapshot.storage.k8s.io/v1beta1 \n" > snapshot.yaml
printf "kind: VolumeSnapshot \n" >> snapshot.yaml
printf "metadata: \n" >> snapshot.yaml
printf "  name: $snapshot_name \n" >> snapshot.yaml
printf "  namespace: $snapshot_namespace \n" >> snapshot.yaml
printf "spec: \n" >> snapshot.yaml
printf "  source: \n" >> snapshot.yaml
printf "    persistentVolumeClaimName: $pvc_name" >> snapshot.yaml

#Create snapshot
gcloud auth activate-service-account $account_service_email --key-file=$account_service_json_dir --project=$gcp_project_name
gcloud container clusters get-credentials $cluster_name --zone $cluster_zone --project $gcp_project_name
kubectl create -f snapshot.yaml --save-config

#Remove old versions
snapshot_count=`kubectl get volumesnapshot | grep prod-psql-snapshot | wc -l`
digest_array=(`kubectl get volumesnapshot | grep prod-psql-snapshot | awk '{print $1}'`)

if [ $snapshot_count -gt 14 ]
then
  oldest_snapshot=`echo ${digest_array[0]}`
  kubectl delete volumesnapshot $oldest_snapshot
fi

Lo que hago con este script es generar el YAML de un VolumeSnapshot, me autentico con la cuenta de servicio y creo ese snapshot. Para dejarlo todo limpio, cuento el número de copias al finalizar, y si pasan de 14, borro las últimas. Para más info acerca de como trabajar con los VolumeSnapshots en GCP, podéis consultarlo aquí.

CronJob

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: snapshot
spec:
  schedule: "01 0 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: gcr.io/[project-name]/[image_name]
              args:
                - /bin/sh
                - -c
                - sh /scripts/snapshot_creation.sh
          restartPolicy: Never

Y ya estaría! Problema resuelto!

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!

Script en bash para eliminar imágenes del Container Registry de GCP

Hoy una entrada sencilla pero efectiva. Al venir de sistemas, me cuesta más que a un desarrollador elaborar código. Y por eso es que me siento tan orgullosa de mis scripts.

En esta ocasión, utilizo la herramienta de comandos en linea de GCP, glcoud.

Para listar:

gcloud container images list-tags [HOSTNAME]/[PROJECT-ID]/[IMAGE_NAME]

Para eliminar:

gcloud container images delete [HOSTNAME]/[PROJECT-ID]/[IMAGE_NAME]@sha256:DIGEST --quiet

El script quedaría tal que así:

#!/bin/bash

declare -a digest_array

#Recoger todas imágenes del repositorio pasado en la llamada al script. Con awk recogemos solo la primera columna que corresponde con el DIGEST.
digest_array=(`gcloud container images list-tags "$1" | awk '{print $1}'`)

#En nuestro caso, queremos conservar las 3 imágenes más recientes. Por ello, el contador es 4 (columna del encabezado + 3 imágenes).
for i in "${digest_array[@]:4}"
        do `gcloud container images delete "$1"@sha256:"$i" --quiet --force-delete-tags`
done

La llamada por terminal, sería:

./delete_old_images.sh "[HOSTNAME]/[PROJECT-ID]/[IMAGE_NAME]"

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.

Snipe-It Docker

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!

Subir imagen docker local al Container Registry de GCP

En mis primeras semanas de andanzas con la plataforma de Google Cloud, me metieron de lleno a los leones. «Ves buscando información sobre docker y Kubernetes, a ver como podemos montar ahí cosas».

Uno de los fundamentales para poder empezar a trabajar de manera cómoda con el entorno GKE es utilizar el Container Registry. Y aunque la documentación de Google es copiosa y viene con muchos ejemplos, etc. me llevó sus días aprender a funcionar de manera ágil.

¿Qué es el Container Registry?

Empecemos por el principio. El Conainer Registry es la heramienta que necesitas utilizar si quieres gestionar imágenes Docker y poder empezar a trabajar con Kubernetes.

Entre las muchas funcionalidades que brinda, está la capacidad de realizar análisis de vulnerabilidades, además de la gestión de los accesos, integración con herramientas tipo Jenkins de CI/CD…

Todo esto esta muy bien Laura, pero… ¡Yo lo que quiero es poder subir una imágen para ir desplegar mi primer pod!

Requisitos previos

Lo más cómodo en GCP es tener descargada su herramienta de consola SDK. No solo para la siguiente implementación, sino para implementaciones sucesivas. Para ello necesitarás:

Si ya has instalado el SDK de Cloud, recuerda actualizarlo para tener siempre la última versión. Para ello, puedes lanzar desde la consola SDK el siguiente comando:

gcloud components update
  • Habilitar la API Cloud BuildAPI
  • Tener creado un proyecto en la plataforma. Si aún no lo tienes, aquí te dejo un enlace a la documentación oficial.
  • Tener instalado docker en local.

Autentícate en SDK

Antes de poder subir nada poder subir nada, necesitamos autenticarnos.

gcloud auth login

Lo siguiente que tenemos que hacer es seleccionar el proyecto con el que empezar a trabajar.

gcloud config set project [PROJECT_ID]

Donde [PROJECT_ID] es tu ID del proyecto de GCP.

Procedimiento

1. Abrir la herramienta de comandos favorita (en mi caso, PowerShell)
2. Re-etiquetar la imágen con el formato adecuado. La sintaxis es la siguiente, dependiendo si le añadís o no la etiqueta:

docker tag [IMAGEN_LOCAL] [REPOSITORIO]/[ID-PROYECTO]/[IMAGEN] 
docker tag [IMAGEN_LOCAL] [REPOSITORIO]/[ID-PROYECTO]/[IMAGEN]:[ETIQUETA]

Los repositorios disponibles para Container Registry son:

  • gcr.io
  • us.gcr.io
  • eu.gcr.io
  • asia.gcr.io.

Ejemplo

docker tag mongo gcr.io/mi-proyecto/mongo

3. Enviar la imagen (push) al Container Registry.

docker push [HOSTNAME]/[PROJECT-ID]/[IMAGE]
docker push [HOSTNAME]/[PROJECT-ID]/[IMAGE]:[TAG]

Ejemplo

docker push gcr.io/mi-proyecto/mongo

4. Comprobar que la subida. Si todo ha ido bien, veremos una nueva entrada en la consola web.

Problema resuelto!

Scroll hacia arriba