Skip to main content

Environments y Anclas

Imaginemos que tenemos 2 ramas (Protegidas) con la siguiente regla en nuestro equipo de desarrollo.

  • develop: todo lo que está en esta rama debe ser desplegado en el entorno de desarrollo.
  • main: esta rama será la productiva.

Podemos definir la misma variable, por ejemplo, TOKEN que vamos a utilizar para hacer algo. Pero si es en el entorno de desarrollo TOKEN=develop123 y si es en el entorno de producción TOKEN=B42LKJL592U0452.

¿Cómo podemos hacer esto?

Al añadir una variable solo tenemos disponible all. En el repositorio, ve a Operate > Environments y crea dos environments; develop y production.

alt text

alt text

Existen muchas más funcionalidades encima de esto, pero por ahora solo necesitamos definir esos entornos y nada más.

Ahora crea la variable TOKEN para cada uno de los entornos pues podrás elegir.

alt text

alt text

Comencemos por este esqueleto para hablar de environment...

stages:
- check
- build
- deploy # Añadido

deploy-develop:
stage: deploy
environment:
name: develop
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
rules:
- cuando sea aceptado el merge para develop

deploy-production:
stage: deploy
environment:
name: production
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
rules:
- cuando sea aceptado el merge para main

Hasta ahora nuestros pasos fueron solo de CI ¿verdad? Entonces se ejecutan para verificar las cosas para que un merge pueda ser aprobado. Después de que un merge sea aprobado y aceptado es que el código se fusiona de hecho.

¿Si un merge es aceptado qué significa eso?

Cuando un merge request (MR) es aceptado (merged), lo que sucede es un push automático hecho por GitLab a la rama de destino (develop, main, etc.) con el contenido resultante del merge.

Aceptar un MR = GitLab hace un git merge y da un push en la rama objetivo

Esto generará otro pipeline con un nuevo evento. Vamos a profundizar aún más en esto, pero es bueno que entiendas este escenario, entonces vamos a completar nuestra rule.

deploy-develop:
stage: deploy
environment:
name: develop
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'

deploy-production:
stage: deploy
environment:
name: production
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'

Con esta regla, si el commit es en main o en develop, sin importar de qué forma sucedió (directo o vía merge request), entonces el job participará en el workflow del pipeline.

Lo correcto es que venga a través de un merge, pero un git push directo de alguien podría hacerse. Podemos sortear esto protegiendo las ramas, pero también podríamos garantizar en las rules en caso de que alguien desproteja.

Al crear el merge request tenemos el siguiente pipeline ejecutándose.

alt text

El evento aquí fue de merge request para main y no de commit en main o en develop.

Todo el workflow (secuencia de jobs) del pipeline se genera antes de crear los jobs. Pero antes de aceptar este merge request que dispararía el stage (falso) solo de deploy vamos a mejorar un poquito más nuestro código y conocer otro método de reutilización de código.

Los dos jobs son muy parecidos podemos definir develop y solo ajustar la diferencia en main. Vamos a conocer el concepto de ancla.

deploy-develop: &deploy-develop  # Definiendo el ancla aquí
image: alpine
stage: deploy
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
environment:
name: develop

deploy-production:
<<: *deploy-develop # Heredando la configuración de deploy-develop
environment:
name: production # Cambiando para el entorno de producción
rules:
- if: '$CI_COMMIT_BRANCH == "main"' # Alterando la regla para la rama main

Aquí reutilizamos poco código, pero este es un escenario típico que da para reducir muchas cosas.

Extends vs Ancla

Extends y anclas (& y <<) pueden parecer similares a primera vista, pues ambos se usan para reutilizar configuraciones en GitLab CI, pero hay algunas diferencias importantes entre ellos.

El extends permite que un job herede configuraciones de otro job. GitLab trata esto de una forma más orientada a herencia de estructura de jobs, como harías con clases en programación orientada a objetos.

  • Más fácil de fusionar la lista de extends.

Las anclas en YAML con el operador << son un recurso del propio YAML y no son exclusivos de GitLab CI. Permiten que repitas configuraciones definiendo un ancla y referenciándola en diferentes partes del archivo YAML.

  • Uso más flexible: Son más genéricas y pueden usarse para cualquier parte del YAML.
  • Permite merge de hashes: El operador << realiza un merge de hashes, es decir, simplemente incorpora el contenido de un ancla en otro lugar, sin crear un vínculo tan explícito como el extends.
  • Simplicidad y lectura: Menos complejidad, ya que no estás creando una estructura de herencia, solo estás reutilizando un conjunto de configuraciones.

El YAML se procesa de arriba abajo. Esto significa que es necesario primero crear el ancla para después usarla. PERO ESTO TAMBIÉN ES VÁLIDO PARA EL EXTENDS, LA PLANTILLA DEBE ESTAR DEFINIDA ANTES.

Ejemplo de uso de ancla.

deploy-develop: &deploy-develop  # Definiendo el ancla aquí
stage: deploy
image: alpine
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
environment:
name: develop
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'

deploy-production:
<<: *deploy-develop # Heredando la configuración de deploy-develop
environment:
name: production # Cambiando para el entorno de producción
rules:
- if: '$CI_COMMIT_BRANCH == "main"' # Alterando la regla para la rama main

Una cosa que las anclas hacen que extends no hace. Ejemplo tonto, pero que refleja todo.

bloquedeteclados: &bloquedeteclados
name: production
scriptdeploy: &scriptdeploy
- |
echo "iniciando script"
echo "terminando script"
deploy-production:
stage: deploy
image: alpine
environment:
<<: *bloquedeteclados # Inyección directa sin necesidad de definir la estructura entera.
script:
<<: *scriptdeploy # Inyección directa solo del código.
rules:
- if: '$CI_COMMIT_BRANCH == "main"'

Si fuéramos a hacer igual usando las plantillas y extends es necesario definir toda la estructura del yaml.

# Plantilla solo para el script
.script-template:
script: # Necesidad de pasar el bloque del script
- echo "iniciando script"
- echo "terminando script"

# Plantilla solo para el environment
.environment-template:
environment: # Necesidad de pasar el bloque del environment
name: production

deploy-production:
extends:
- .script-template # Traerá el bloque entero
- .environment-template # Traerá el bloque entero
stage: deploy
image: alpine
rules:
- if: '$CI_COMMIT_BRANCH == "main"' # Condición para ejecutar el job en la rama main

Ahora vamos a hablar de la realidad. Busca siempre usar el extends por algunas razones:

  • Es más expresivo, confiable y mejor soportado por GitLab.
  • Las anclas no tienen soporte a validación en GitLab y los errores pueden ser difíciles de debugar.
  • Extends es recurso nativo y mantenido por GitLab, forma parte del mecanismo oficial de CI/CD.
  • GitLab recomienda el uso de extends.
  • GitLab no controla el comportamiento del YAML, solo interpreta lo que llega.

¿Cómo quedaría nuestro resultado final de acuerdo con las buenas prácticas?

.deploy:
stage: deploy
image: alpine
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
environment:
name: develop
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'

deploy-develop:
extends: .deploy

deploy-production:
extends: .deploy
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'

Pero podríamos definir esa variable production en tiempo de ejecución de acuerdo con la rama y ya eliminamos esa plantilla ahí.

deploy:
stage: deploy
image: alpine
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
environment:
name: $DEPLOY_ENV
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
variables:
DEPLOY_ENV: develop
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
DEPLOY_ENV: production

Haz commit de todo y abre el merge para verificar los dos pipelines, uno del merge request y otro del commit tan pronto aceptes el merge.

alt text

alt text

alt text

Default

Si observamos la definición de abajo, funciona como una plantilla implícita, incluso no siendo un .default. Una vez definido, todos los jobs por defecto hacen el extends de esto.

default:
tags:
- general
image: node:22-alpine