Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

¡Hola! En los últimos tiempos han salido muchas herramientas geniales de automatización tanto para construir imágenes Docker como para desplegar en Kubernetes. Por ello, decidí jugar un poco con GitLab, estudiar sus capacidades adecuadamente y, por supuesto, configurar un pipeline.

La inspiración para este trabajo fue el sitio kubernetes.io, que se genera a partir de los códigos fuente automáticamente, y para cada solicitud de extracción enviada, el robot genera automáticamente una versión previa del sitio con tus cambios y proporciona un enlace para su visualización.

He tratado de establecer un proceso similar desde cero, pero completamente basado en GitLab CI y herramientas gratuitas que estoy acostumbrado a utilizar para desplegar aplicaciones en Kubernetes. Hoy, por fin, les contaré más al respecto.

En el artículo se discutirán herramientas como:
Hugo, qbec, kaniko, git-crypt y GitLab CI con la creación de entornos dinámicos.

Contenido

  1. Introducción a Hugo
  2. Preparación del Dockerfile
  3. Introducción a kaniko
  4. Introducción a qbec
  5. Probamos GitLab Runner con el ejecutor de Kubernetes
  6. Despliegue de gráficos Helm con qbec
  7. Introducción a git-crypt
  8. Creamos una imagen toolbox
  9. Nuestro primer pipeline y construcción de imágenes por etiquetas
  10. Automatización del despliegue
  11. Artefactos y construcción al hacer push en master
  12. Entornos dinámicos
  13. Aplicaciones de revisión

1. Introducción a Hugo

Como ejemplo de nuestro proyecto, intentaremos crear un sitio para publicar documentación, construido sobre Hugo. Hugo es un generador de contenido estático.

Para aquellos que no están familiarizados con los generadores de sitios estáticos, les contaré un poco más al respecto. A diferencia de los motores de sitios convencionales con bases de datos y algún tipo de PHP, que generan páginas al vuelo cuando un usuario realiza una solicitud, los generadores estáticos funcionan de manera algo diferente. Permiten tomar los archivos fuente, que suelen ser un conjunto de archivos en formato Markdown y plantillas, y luego compilarlo en un sitio completamente listo.

Es decir, al final obtendrás una estructura de directorios y un conjunto de archivos HTML generados, que podrás simplemente subir a cualquier host barato y obtener un sitio web funcional.

Puedes instalar Hugo localmente y probarlo:

Inicializa un nuevo sitio:

hugo new site docs.ejemplo.org

Y a su vez, el repositorio git:

cd docs.ejemplo.org
git init

Por ahora, nuestro sitio está completamente limpio y para que aparezca algo en él, primero necesitamos conectar un tema, que no es más que un conjunto de plantillas y reglas definidas por las que se genera nuestro sitio.

Como tema utilizaremos Learn, que, en mi opinión, es perfecto para un sitio de documentación.

Es importante destacar que no necesitamos guardar los archivos del tema en el repositorio de nuestro proyecto, en su lugar, simplemente podemos conectarlo usando git submodule:

git submodule add https://github.com/matcornic/hugo-theme-learn themes/learn

De esta manera, nuestro repositorio solo contendrá los archivos directamente relacionados con nuestro proyecto, y el tema conectado quedará como un enlace a un repositorio específico y a un commit en él, es decir, siempre se podrá extraer de la fuente original sin temor a cambios incompatibles.

Ajustemos la config config.toml:

baseURL = "http://docs.example.org/"
languageCode = "es"
title = "Mi sitio de Docs"
theme = "learn"

Ya en esta etapa podemos arrancar:

hugo server

Y en la dirección http://localhost:1313/ comprobar nuestro sitio recién creado, todos los cambios realizados en el directorio se actualizan automáticamente en la página abierta en el navegador, ¡muy conveniente!

Intentemos crear una página de título en content/_index.md:

# My docs site

## Welcome to the docs!

You will be very smart :-)

Captura de pantalla de la página recién creada

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

Para generar el sitio, solo es necesario ejecutar:

hugo

El contenido del directorio public/ será tu sitio web.
Sí, por cierto, vamos a incluirlo inmediatamente en .gitignore:

echo /public > .gitignore

No olvidemos hacer commit de nuestros cambios:

git add .
git commit -m "Nuevo sitio creado"

2. Preparación del Dockerfile

Es hora de definir la estructura de nuestro repositorio. Normalmente uso algo como:

.
├── deploy
│   ├── app1
│   └── app2
└── dockerfiles
    ├── image1
    └── image2

  • dockerfiles/ — contiene directorios con Dockerfiles y todo lo necesario para construir nuestras imágenes de docker.
  • deploy/ — contiene directorios para desplegar nuestras aplicaciones en Kubernetes

Así que nuestro primer Dockerfile lo crearemos en dockerfiles/website/Dockerfile

FROM alpine:3.11 as builder
ARG HUGO_VERSION=0.62.0
RUN wget -O- https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-64bit.tar.gz | tar -xz -C /usr/local/bin
ADD . /src
RUN hugo -s /src

FROM alpine:3.11
RUN apk add --no-cache darkhttpd
COPY --from=builder /src/public /var/www
ENTRYPOINT [ "/usr/bin/darkhttpd" ]
CMD [ "/var/www" ]

Como puedes notar, el Dockerfile contiene dos FROM, esta posibilidad se llama multi-stage build y permite excluir del docker final todo lo innecesario.
Así que la imagen final solo contendrá darkhttpd (un servidor HTTP ligero) y public/ — el contenido de nuestro sitio web generado estáticamente.

No olvidemos hacer commit de nuestros cambios:

git add dockerfiles/website
git commit -m "Agregar Dockerfile para el sitio web"

3. Introducción a kaniko

Como constructor de imágenes de docker, decidí utilizar kaniko, ya que su funcionamiento no requiere la presencia del demonio de docker, y la construcción se puede llevar a cabo en cualquier máquina, almacenando la caché directamente en el registro, evitando así la necesidad de tener un almacenamiento persistente completo.

Para construir la imagen, es suficiente con iniciar un contenedor con kaniko executor y pasarle el contexto de construcción actual; esto se puede hacer localmente, a través de docker:

docker run -ti --rm 
  -v $PWD: /workspace 
  -v ~/.docker/config.json:/kaniko/.docker/config.json:ro 
  gcr.io/kaniko-project/executor:v0.15.0 
  --cache 
  --dockerfile=dockerfiles/website/Dockerfile 
  --destination=registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1

Donde registry.gitlab.com/kvaps/docs.example.org/website — el nombre de su imagen de docker; después de la construcción, se subirá automáticamente al registro de docker.

Parámetro —cache permite almacenar en caché las capas en el registro de docker; para el ejemplo mencionado, se guardarán en registry.gitlab.com/kvaps/docs.example.org/website/cache, aunque puede especificar otra ruta utilizando el parámetro —cache-repo.

Captura de pantalla del docker-registry

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

4. Introducción a qbec

Qbec es una herramienta de despliegue que permite describir de manera declarativa los manifiestos de su aplicación y desplegarlos en Kubernetes. El uso de Jsonnet como sintaxis principal simplifica notablemente la descripción de las diferencias para múltiples entornos, además de casi eliminar la repetitividad del código.

Esto puede ser especialmente relevante en aquellos casos en que necesite desplegar una aplicación en varios clústeres con diferentes parámetros y desea describirlos declarativamente en Git.

Qbec también permite renderizar gráficos de Helm pasando los parámetros necesarios y luego operando con ellos como si fueran manifiestos normales; incluso se pueden aplicar diferentes mutaciones, lo que a su vez elimina la necesidad de usar ChartMuseum. Es decir, puede almacenar y renderizar gráficos directamente desde git, donde realmente pertenecen.

Como mencioné antes, almacenaremos todos los despliegues en el directorio deploy/:

mkdir deploy
cd deploy

Vamos a inicializar nuestra primera aplicación:

qbec init website
cd website

Ahora la estructura de nuestra aplicación se ve así:

.
├── components
├── environments
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
└── qbec.yaml

veamos el archivo qbec.yaml:

apiVersion: qbec.io/v1alpha1
kind: App
metadata:
  name: website
spec:
  environments:
    default:
      defaultNamespace: docs
      server: https://kubernetes.example.org:8443
  vars: {}

Aquí nos interesa principalmente spec.environments, qbec ya ha creado un entorno predeterminado para nosotros y ha tomado la dirección del servidor, así como el namespace de nuestra configuración kubeconfig actual.
Ahora, al desplegar en default el entorno, qbec siempre desplegará solo en el clúster de Kubernetes especificado y en el namespace indicado, lo que significa que ya no tendrás que cambiar entre contextos y namespaces para realizar un despliegue.
En caso de ser necesario, siempre puedes actualizar la configuración en este archivo.

Todos tus entornos se describen en qbec.yaml, y en el archivo params.libsonnet, donde se especifica de dónde deben tomarse los parámetros para ellos.

A continuación, vemos dos directorios:

  • components/ — aquí se almacenarán todos los manifiestos para nuestra aplicación, que pueden estar descritos tanto en jsonnet como en archivos yaml normales.
  • environments/ — aquí describiremos todas las variables (parámetros) para nuestros entornos.

Por defecto, tenemos dos archivos:

  • environments/base.libsonnet — contendrá parámetros comunes para todos los entornos.
  • environments/default.libsonnet — contiene parámetros sobreescritos para el entorno. default

Vamos a abrir environments/base.libsonnet y agregar ahí parámetros para nuestro primer componente:

{
  components: {
    website: {
      name: 'example-docs',
      image: 'registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1',
      replicas: 1,
      containerPort: 80,
      servicePort: 80,
      nodeSelector: {},
      tolerations: [],
      ingressClass: 'nginx',
      domain: 'docs.example.org',
    },
  },
}

Creemos también nuestro primer componente components/website.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '.. / params.libsonnet';
local params = p.components.website;

[
  {
    apiVersion: 'apps/v1',
    kind: 'Deployment',
    metadata: {
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      replicas: params.replicas,
      selector: {
        matchLabels: {
          app: params.name,
        },
      },
      template: {
        metadata: {
          labels: { app: params.name },
        },
        spec: {
          containers: [
            {
              name: 'darkhttpd',
              image: params.image,
              ports: [
                {
                  containerPort: params.containerPort,
                },
              ],
            },
          ],
          nodeSelector: params.nodeSelector,
          tolerations: params.tolerations,
          imagePullSecrets: [{ name: 'regsecret' }],
        },
      },
    },
  },
  {
    apiVersion: 'v1',
    kind: 'Service',
    metadata: {
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      selector: {
        app: params.name,
      },
      ports: [
        {
          port: params.servicePort,
          targetPort: params.containerPort,
        },
      ],
    },
  },
  {
    apiVersion: 'extensions/v1beta1',
    kind: 'Ingress',
    metadata: {
      annotations: {
        'kubernetes.io/ingress.class': params.ingressClass,
      },
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      rules: [
        {
          host: params.domain,
          http: {
            paths: [
              {
                backend: {
                  serviceName: params.name,
                  servicePort: params.servicePort,
                },
              },
            ],
          },
        },
      ],
    },
  },
]

En este archivo hemos descrito tres entidades de Kubernetes a la vez: Deployment, Servicio y IngressSi lo deseamos, podríamos separarlas en diferentes componentes, pero en esta etapa nos basta con una sola.

Sintaxis jsonnet se parece mucho al json normal, de hecho, un json normal ya es un jsonnet válido, así que al principio puede ser más fácil usar servicios en línea como yaml2json para convertir tu yaml familiar a json, o bien, si tus componentes no contienen variables, se pueden describir como un yaml normal.

Al trabajar con jsonnet Te recomiendo instalar un complemento para tu editor.

Por ejemplo, para vim hay un complemento llamado vim-jsonnet, que incluye resaltado de sintaxis y ejecuta automáticamente jsonnet fmt en cada guardado (requiere que jsonnet esté instalado).

Todo listo, ahora podemos comenzar a desplegar:

Para ver lo que hemos obtenido, ejecutaremos:

qbec show default

En la salida verás los manifiestos yaml renderizados que se aplicarán en el clúster por defecto.

Perfecto, ahora aplicaremos:

qbec apply default

En la salida siempre verás lo que se hará en tu clúster, qbec te pedirá que confirmes los cambios escribiendo y podrás confirmar tus intenciones.

Listo, ¡ahora nuestra aplicación está desplegada!

Si realizas cambios, siempre podrás ejecutar:

qbec diff default

para ver cómo estos cambios afectarán al despliegue actual

No olvidemos hacer commit de nuestros cambios:

cd ..\/..
git add deploy\/website
git commit -m "Añadir despliegue para el sitio web"

5. Probando Gitlab-runner con Kubernetes-executor

Hasta hace poco, solo usaba el convencional gitlab-runner en una máquina preparada de antemano (contenedor LXC) con shell o docker-executor. Inicialmente teníamos varios runners definidos globalmente en nuestro GitLab. Ellos construían imágenes de Docker para todos los proyectos.

Pero como ha demostrado la práctica, esta opción no es la más ideal, tanto en términos de practicidad como de seguridad. Es mucho mejor y ideológicamente correcto tener runners separados desplegados para cada proyecto, o incluso para cada entorno.

Afortunadamente, esto no es un problema, ya que ahora vamos a desplegar gitlab-runner directamente como parte de nuestro proyecto en Kubernetes.

GitLab proporciona un chart helm listo para el despliegue de gitlab-runner en Kubernetes. Así que todo lo que necesitas hacer es conocer el token de registro para nuestro proyecto en Settings —> CI \/ CD —> Runners y pasarlo a helm:

helm repo add gitlab https:\/\/charts.gitlab.io

helm install gitlab-runner 
  --set gitlabUrl=https:\/\/gitlab.com 
  --set runnerRegistrationToken=yga8y-jdCusVDn_t4Wxc 
  --set rbac.create=true 
  gitlab\/gitlab-runner

Donde:

  • https://gitlab.com — la dirección de tu servidor GitLab.
  • yga8y-jdCusVDn_t4Wxc — el token de registro para tu proyecto.
  • rbac.create=true — proporciona al runner la cantidad necesaria de privilegios para poder crear pods para ejecutar nuestras tareas con el kubernetes-executor.

Si todo se ha hecho correctamente, deberías ver el runner registrado en la sección Runners, en la configuración de tu proyecto.

Captura de pantalla del runner añadido

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

¿Tan fácil? — ¡Sí, tan fácil! Sin más complicaciones con el registro manual de runners, a partir de ahora los runners se crearán y destruirán automáticamente.

6. Despliegue de Helm-charts con QBEC

Dado que hemos decidido considerar gitlab-runner parte de nuestro proyecto, es hora de describirlo en nuestro repositorio Git.

Podríamos describirlo como un componente separado sitio_web, pero más adelante planeamos desplegar diferentes copias sitio_web muy a menudo, a diferencia de gitlab-runner, que solo se desplegará una vez en cada clúster de Kubernetes. Así que iniciemos una aplicación separada para ello:

cd deploy
qbec init gitlab-runner
cd gitlab-runner

Esta vez no describiremos las entidades de Kubernetes manualmente, sino que utilizaremos un chart de Helm listo. Una de las ventajas de qbec es la posibilidad de renderizar charts de Helm directamente desde un repositorio de Git.

Conectémoslo usando un submódulo de git:

git submodule add https://gitlab.com/gitlab-org/charts/gitlab-runner vendor/gitlab-runner

Ahora el directorio vendor/gitlab-runner contiene nuestro repositorio con el chart para gitlab-runner.

De manera similar se pueden conectar otros repositorios, por ejemplo, el repositorio completo con charts oficiales. https://github.com/helm/charts

Describamos el componente components/gitlab-runner.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.gitlabRunner;

std.native('expandHelmTemplate')(
  '../vendor/gitlab-runner',
  params.values,
  {
    nameTemplate: params.name,
    namespace: env.namespace,
    thisFile: std.thisFile,
    verbose: true,
  }
)

El primer argumento a expandHelmTemplate es la ruta al chart, luego params.values, que obtendremos de los parámetros del entorno, y luego viene un objeto con

  • nameTemplate — el nombre de la versión
  • namespace — el namespace pasado a Helm
  • thisFile — un parámetro obligatorio que pasa la ruta al archivo actual
  • verbose — muestra el comando helm template con todos los argumentos al renderizar el chart.

Ahora describamos los parámetros para nuestro componente en environments/base.libsonnet:

local secrets = import '../secrets/base.libsonnet';

{
  components: {
    gitlabRunner: {
      name: 'gitlab-runner',
      values: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          create: true,
        },
        runnerRegistrationToken: secrets.runnerRegistrationToken,
      },
    },
  },
}

Presta atención runnerRegistrationToken lo tomamos de un archivo externo secrets/base.libsonnet, vamos a crearlo:

{
  runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}

Verifiquemos si todo funciona:

qbec show default

si todo está bien, podemos eliminar nuestra versión anterior, desplegada a través de Helm:

helm uninstall gitlab-runner

y desplegar la misma, pero ya a través de qbec:

qbec apply default

7. Introducción a git-crypt

Git-crypt es una herramienta que permite configurar un cifrado transparente para su repositorio.

En este momento, la estructura de nuestro directorio para gitlab-runner se ve así:

.
├── components
│   ├── gitlab-runner.jsonnet
├── environments
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
├── qbec.yaml
├── secrets
│   └── base.libsonnet
└── vendor
    └── gitlab-runner (submódulo)

Pero no es seguro almacenar secretos en Git, ¿verdad? Así que necesitamos cifrarlos adecuadamente.

Normalmente, no siempre tiene sentido hacerlo solo por una variable. Puede pasar secretos en qbec y a través de variables de entorno de su sistema CI.
Sin embargo, es importante señalar que hay proyectos más complejos que pueden contener muchos más secretos, y transmitirlos todos a través de variables de entorno sería extremadamente difícil.

Además, en tal caso no podría hablarles de una herramienta tan maravillosa como git-crypt.

git-crypt también es conveniente porque permite guardar todo el historial de secretos, así como comparar, fusionar y resolver conflictos de la misma manera que estamos acostumbrados a hacer con Git.

Lo primero que debemos hacer después de la instalación git-crypt es generar las claves para nuestro repositorio:

git crypt init

Si tienes una clave PGP, puedes agregarte de inmediato como colaborador en este proyecto:

git-crypt add-gpg-user kvapss@gmail.com

De esta manera, siempre podrás descifrar este repositorio utilizando tu clave privada.

Si no tienes una clave PGP y no se prevé que la consigas, puedes optar por otro camino y exportar la clave del proyecto:

git crypt export-key /path/to/keyfile

Así, cualquiera que tenga el keyfile podrá descifrar tu repositorio.

Es hora de configurar nuestro primer secreto.
Recordemos, seguimos en el directorio deploy/gitlab-runner/, donde tenemos un directorio secrets/, así que vamos a cifrar todos los archivos en él, para eso crearemos el archivo secrets/.gitattributes con el siguiente contenido:

* filter=git-crypt diff=git-crypt
.gitattributes !filter !diff

Como se puede ver en el contenido, todos los archivos que coincidan con la máscara * serán procesados a través de git-crypt, excepto por el propio .gitattributes

Podemos verificar esto ejecutando:

git crypt status -e

La salida nos dará una lista de todos los archivos en el repositorio para los cuales la encriptación está activada.

Eso es todo, ahora podemos hacer commit de nuestros cambios:

cd ../..
git add .
git commit -m "Add deploy for gitlab-runner"

Para bloquear el repositorio, simplemente ejecuta:

git crypt lock

y de inmediato todos los archivos encriptados se convertirán en un conjunto de datos binarios, será imposible leerlos.
Para descifrar el repositorio, ejecuta:

git crypt unlock

8. Creamos la imagen de toolbox

La imagen de toolbox es una imagen con todas las herramientas que utilizaremos para implementar nuestro proyecto. Se usará con GitLab Runner para realizar tareas estándar de implementación.

Aquí es simple, creamos un nuevo dockerfiles/toolbox/Dockerfile con el siguiente contenido:

DE alpine:3.11

RUN apk add --no-cache git git-crypt

RUN QBEC_VER=0.10.3 
 && wget -O- https://github.com/splunk/qbec/releases/download/v${QBEC_VER}/qbec-linux-amd64.tar.gz 
     | tar -C /tmp -xzf - 
 && mv /tmp/qbec /tmp/jsonnet-qbec /usr/local/bin/

RUN KUBECTL_VER=1.17.0 
 && wget -O /usr/local/bin/kubectl 
      https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/linux/amd64/kubectl 
 && chmod +x /usr/local/bin/kubectl

RUN HELM_VER=3.0.2 
 && wget -O- https://get.helm.sh/helm-v${HELM_VER}-linux-amd64.tar.gz 
     | tar -C /tmp -zxf - 
 && mv /tmp/linux-amd64/helm /usr/local/bin/helm

Como puedes notar, en esta imagen estamos instalando todas las utilidades que utilizamos para desplegar nuestra aplicación. Aquí no necesitamos, salvo que kubectl, pero quizás quieras experimentar con esto durante la configuración del pipeline.

Además, para poder comunicarnos con Kubernetes y realizar un despliegue, necesitamos configurar un rol para los pods generados por gitlab-runner.

Para esto, naveguemos al directorio con gitlab-runner:

cd deploy/gitlab-runner

y añadamos un nuevo componente components/rbac.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.rbac;

[
  {
    apiVersion: 'v1',
    kind: 'ServiceAccount',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
  },
  {
    apiVersion: 'rbac.authorization.k8s.io/v1',
    kind: 'Role',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
    rules: [
      {
        apiGroups: [
          '*',
        ],
        resources: [
          '*',
        ],
        verbs: [
          '*',
        ],
      },
    ],
  },
  {
    apiVersion: 'rbac.authorization.k8s.io/v1',
    kind: 'RoleBinding',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
    roleRef: {
      apiGroup: 'rbac.authorization.k8s.io',
      kind: 'Role',
      name: params.name,
    },
    subjects: [
      {
        kind: 'ServiceAccount',
        name: params.name,
        namespace: env.namespace,
      },
    ],
  },
]

También describiremos nuevos parámetros en environments/base.libsonnet, que ahora se ve así:

local secrets = import '../secrets/base.libsonnet';

{
  components: {
    gitlabRunner: {
      name: 'gitlab-runner',
      values: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          create: true,
        },
        runnerRegistrationToken: secrets.runnerRegistrationToken,
        runners: {
          serviceAccountName: $.components.rbac.name,
          image: 'registry.gitlab.com/kvaps/docs.example.org/toolbox:v0.0.1',
        },
      },
    },
    rbac: {
      name: 'gitlab-runner-deploy',
    },
  },
}

Presta atención $.components.rbac.name se refiere a name para el componente rbac

Vamos a verificar qué ha cambiado:

qbec diff default

y aplicaremos nuestros cambios en Kubernetes:

qbec apply default

Tampoco olvidemos confirmar nuestros cambios en git:

cd ../../
git add dockerfiles/toolbox
git commit -m "Añadir Dockerfile para toolbox"
git add deploy/gitlab-runner
git commit -m "Configurar gitlab-runner para usar toolbox"

9. Nuestro primer pipeline y construcción de imágenes por etiquetas

En la raíz del proyecto crearemos .gitlab-ci.yml con el siguiente contenido:

.build_docker_image:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug-v0.15.0
    entrypoint: [""]
  before_script:
    - echo "{"auths":{"$CI_REGISTRY":{"username":"$CI_REGISTRY_USER","password":"$CI_REGISTRY_PASSWORD"}}}" > /kaniko/.docker/config.json

build_toolbox:
  extends: .build_docker_image
  script:
    - /kaniko/executor --cache --context $CI_PROJECT_DIR/dockerfiles/toolbox --dockerfile $CI_PROJECT_DIR/dockerfiles/toolbox/Dockerfile --destination $CI_REGISTRY_IMAGE/toolbox:$CI_COMMIT_TAG
  only:
    refs:
      - tags

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_TAG
  only:
    refs:
      - tags

Tenga en cuenta que estamos usando GIT_SUBMODULE_STRATEGY: normal para aquellos trabajos donde es necesario inicializar explícitamente los submódulos antes de la ejecución.

No olvidemos hacer commit de nuestros cambios:

git add .gitlab-ci.yml
git commit -m "Automatizar la construcción de docker"

Creo que se puede considerar esto como la versión v0.0.1 y etiquetarlo:

git tag v0.0.1

Etiquetaremos cada vez que necesitemos liberar una nueva versión. Las etiquetas en las imágenes de Docker estarán asociadas con las etiquetas de Git. Cada push con una nueva etiqueta iniciará la construcción de imágenes con esta etiqueta.

Ejecutaremos git push —tags, y echemos un vistazo a nuestra primera canalización:

Captura de pantalla de la primera canalización

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

Es importante destacar que la construcción por etiquetas es adecuada para construir imágenes de Docker, pero no es adecuada para desplegar aplicaciones en Kubernetes. Dado que se pueden asignar nuevas etiquetas a antiguos commits, la inicialización de la canalización para ellos conducirá al despliegue de una versión anterior.

Para resolver este problema, normalmente la construcción de imágenes de Docker se vincula a etiquetas, y el despliegue de aplicaciones a la rama master, en la que se han fijado las versiones de las imágenes construidas. Solo en este caso podrá inicializar un rollback simplemente haciendo un revert master-de la rama.

10. Automatización del despliegue

Para que Gitlab-runner pueda descifrar nuestros secretos, necesitaremos exportar la clave del repositorio y agregarla como variable de entorno en nuestro CI:

git crypt export-key /tmp/docs-repo.key
base64 -w0 /tmp/docs-repo.key; echo

guardaremos la cadena obtenida en Gitlab, para ello iremos a la configuración de nuestro proyecto:
Configuraciones —> CI / CD —> Variables

Y crearemos una nueva variable:

Tipo
Clave
Value
Protegida
Enmascarada
Ámbito

Archivo
GITCRYPT_KEY
<your string>
true (temporalmente durante el aprendizaje puede ser false)
true
Todos los entornos

Captura de pantalla de la variable agregada

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

Ahora actualizaremos nuestro .gitlab-ci.yml agregando en él:

.deploy_qbec_app:
  stage: deploy
  only:
    refs:
      - master

deploy_gitlab_runner:
  extends: .deploy_qbec_app
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  before_script:
    - base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
  script:
    - qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --wait --yes

deploy_website:
  extends: .deploy_qbec_app
  script:
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes

Aquí hemos implementado varias opciones nuevas para qbec:

  • —root some/app — permite especificar el directorio de una aplicación específica
  • —force:k8s-context __incluster__ — es una variable mágica que indica que el despliegue ocurrirá en el mismo clúster donde se ejecuta gitlab-runner. Es necesario hacer esto porque, de lo contrario, qbec intentará encontrar un servidor Kubernetes adecuado en tu kubeconfig
  • —wait — hace que qbec espere a que los recursos que crea cambien a estado Ready y solo luego termine con un exit-code exitoso.
  • —yes — simplemente desactiva el shell interactivo ¿Estás seguro? al desplegar.

No olvidemos hacer commit de nuestros cambios:

git add .gitlab-ci.yml
git commit -m "Automatizar despliegue"

Y después git push veremos cómo se han desplegado nuestras aplicaciones:

Captura de pantalla del segundo pipeline

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

11. Artefactos y construcción al hacer push en master

Generalmente, los pasos descritos anteriormente son suficientes para construir y entregar casi cualquier microservicio, pero no queremos etiquetar cada vez que necesitemos actualizar el sitio. Por lo tanto, optaremos por una forma más dinámica y configuraremos el despliegue por digest en la rama master.

La idea es simple: ahora la imagen de nuestro sitio_web se reconstruirá cada vez que se haga un push en master, y después se desplegará automáticamente en Kubernetes.

Actualicemos estos dos trabajos en nuestro .gitlab-ci.yml:

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - mkdir -p $CI_PROJECT_DIR/artifacts
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
  artifacts:
    paths:
      - artifacts/
  only:
    refs:
      - master
      - tags

deploy_website:
  extends: .deploy_qbec_app
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

Ten en cuenta que hemos agregado la rama master a refs para el trabajo build_website y ahora usamos $CI_COMMIT_REF_NAME en lugar de $CI_COMMIT_TAG, es decir, nos estamos desacoplando de las etiquetas en Git y ahora vamos a subir la imagen con el nombre de la rama de commit que inició el pipeline. Cabe señalar que esto también funcionará con etiquetas, lo que nos permitirá guardar instantáneas del sitio con una versión específica en docker-registry.

Cuando el nombre de la etiqueta de docker para una nueva versión del sitio puede permanecer sin cambios, aún debemos describir los cambios para Kubernetes, de lo contrario, simplemente no volverá a implementar la aplicación desde una nueva imagen, ya que no notará ningún cambio en el manifiesto de implementación.

La opción —vm:ext-str digest="$DIGEST" para qbec — permite pasar una variable externa en jsonnet. Queremos que con cada lanzamiento de nuestra aplicación se vuelva a implementar en el clúster. No podemos usar un nombre de etiqueta que ahora puede permanecer sin cambios, ya que necesitamos vincularnos a una versión específica de la imagen y activar la implementación cuando esta cambie.

Aquí nos ayudará la capacidad de Kaniko de guardar el digest de la imagen en un archivo (opción —digest-file)
Luego, pasaremos este archivo y lo leeremos en el momento de la implementación.

Actualizaremos los parámetros para nuestro deploy/website/environments/base.libsonnet que ahora se verá así:

{
  components: {
    website: {
      name: 'example-docs',
      image: 'registry.gitlab.com/kvaps/docs.example.org/website@' + std.extVar('digest'),
      replicas: 1,
      containerPort: 80,
      servicePort: 80,
      nodeSelector: {},
      tolerations: [],
      ingressClass: 'nginx',
      domain: 'docs.example.org',
    },
  },
}

Listo, ahora cualquier commit en master inicializará la construcción de la imagen de docker para sitio_web, y luego su implementación en Kubernetes.

No olvidemos hacer commit de nuestros cambios:

git add .
git commit -m "Configurar construcción dinámica"

Verificaremos, después git push deberíamos ver algo como esto:

Captura de pantalla de la tubería para master

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

En principio, no necesitamos volver a implementar gitlab-runner en cada push, a menos que, por supuesto, algo haya cambiado en su configuración, vamos a corregir esto en .gitlab-ci.yml:

deploy_gitlab_runner:
  extends: .deploy_qbec_app
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  before_script:
    - base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
  script:
    - qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --wait --yes
  only:
    changes:
      - deploy/gitlab-runner/**/*

changes permitirá rastrear los cambios en deploy/gitlab-runner/ y activará nuestro trabajo solo si hay alguno

No olvidemos hacer commit de nuestros cambios:

git add .gitlab-ci.yml
git commit -m "Reducir implementación de gitlab-runner"

git push, eso es mejor:

Captura de pantalla de la tubería actualizada

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

12. Entornos dinámicos

Ha llegado el momento de diversificar nuestra tubería con entornos dinámicos.

Primero, actualizaremos el trabajo build_website en nuestro .gitlab-ci.yml, eliminando el bloque only, lo que hará que GitLab lo active en cualquier commit en cualquier rama:

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - mkdir -p $CI_PROJECT_DIR/artifacts
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
  artifacts:
    paths:
      - artifacts/

Luego actualizaremos el trabajo deploy_website, añadimos allí un bloque entorno:

deploy_website:
  extends: .deploy_qbec_app
  environment:
    name: prod
    url: https://docs.example.org
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

Esto permitirá a Gitlab asociar el trabajo con prod el entorno y mostrar el enlace correcto a él.

Ahora añadimos dos trabajos más:

deploy_website:
  extends: .deploy_qbec_app
  environment:
    name: prod
    url: https://docs.example.org
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

deploy_review:
  extends: .deploy_qbec_app
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: http://$CI_ENVIRONMENT_SLUG.docs.example.org
    on_stop: stop_review
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply review --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
  only:
    refs:
    - branches
  except:
    refs:
      - master

stop_review:
  extends: .deploy_qbec_app
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop
  stage: deploy
  before_script:
    - git clone "$CI_REPOSITORY_URL" master
    - cd master
  script:
    - qbec delete review --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
  variables:
    GIT_STRATEGY: none
  only:
    refs:
    - branches
  except:
    refs:
      - master
  when: manual

Se ejecutarán al hacer push en cualquier rama excepto master y desplegarán la versión de vista previa del sitio.

Vemos una nueva opción para qbec: —app-tag — permite etiquetar las versiones desplegadas de la aplicación y trabajar solo dentro de esa etiqueta; al crear y destruir recursos en Kubernetes, qbec solo operará con ellas.
De este modo, no necesitamos crear un entorno separado para cada revisión, sino simplemente reutilizar el mismo.

Aquí también utilizamos qbec apply review, en lugar de qbec apply default — este es precisamente el momento en que intentamos describir las diferencias entre nuestros entornos (revisión y predeterminado):

Añadamos el entorno a deploy/website/qbec.yaml

spec:
  environments:
    review:
      defaultNamespace: docs
      server: https://kubernetes.example.org:8443

Luego lo declaramos en deploy/website/params.libsonnet:

local env = std.extVar('qbec.io/env');
local paramsMap = {
  _: import './environments/base.libsonnet',
  default: import './environments/default.libsonnet',
  review: import './environments/review.libsonnet',
};

if std.objectHas(paramsMap, env) then paramsMap[env] else error 'environment ' + env + ' not defined in ' + std.thisFile

Y escribimos parámetros personalizados para él en deploy/website/environments/review.libsonnet:

// this file has the param overrides for the default environment
local base = import './base.libsonnet';
local slug = std.extVar('qbec.io/tag');
local subdomain = std.extVar('subdomain');

base {
  components+: {
    website+: {
      name: 'example-docs-' + slug,
      domain: subdomain + '.docs.example.org',
    },
  },
}

También echemos un vistazo más de cerca al trabajo stop_review, se activará al eliminar una rama y para que gitlab no intente hacer checkout en ella, se utiliza GIT_STRATEGY: none, más tarde clonamos master-la rama y eliminamos la revisión a través de ella.
Es un poco complicado, pero aún no he encontrado una forma más bonita.
Una alternativa podría ser desplegar cada revisión en un espacio de nombres separado, que siempre se puede eliminar por completo.

No olvidemos hacer commit de nuestros cambios:

git add .
git commit -m "Habilitar revisión automática"

git push, git checkout -b prueba, git push origin prueba, comprobamos:

Captura de pantalla de los entornos creados en Gitlab

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

¿Todo funciona? — genial, eliminamos nuestra rama de prueba: git checkout master, git push origin :prueba, verificamos que los trabajos de eliminación del entorno se han ejecutado sin errores.

Aquí quisiera aclarar que cualquier desarrollador del proyecto puede crear ramas, también puede modificar .gitlab-ci.yml el archivo y obtener acceso a las variables secretas.
Por lo tanto, se recomienda encarecidamente permitir su uso solo para ramas protegidas, por ejemplo, en master, o crear un conjunto separado de variables para cada entorno.

13. Aplicaciones de revisión

Aplicaciones de revisión es una funcionalidad de Gitlab que permite agregar un botón para cada archivo en el repositorio para su vista rápida en el entorno desplegado.

Para que aparezcan estos botones, es necesario crear un archivo .gitlab/route-map.yml y describir en él todas las transformaciones de ruta, en nuestro caso esto será muy simple:

# Indices
- source: /content/(.+?)_index.(md|html)/ 
  public: '1'

# Pages
- source: /content/(.+?).(md|html)/ 
  public: '1/'

No olvidemos hacer commit de nuestros cambios:

git add .gitlab/
git commit -m "Habilitar aplicaciones de revisión"

git push, y comprobamos:

Captura de pantalla del botón de aplicación de revisión

Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

¡Trabajo terminado!

Códigos fuente del proyecto:

Gracias por su atención, espero que les haya gustado Probando nuevas herramientas para la compilación y automatización de despliegues en Kubernetes

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