- publicado
Pipelines Dinámicos en GitLab
- authors
Las pipelines estáticas son suficientes para procesos cuyos requerimientos no cambian drástricamente con el tiempo. No te recomiendo empezar tu Integración y Entrega Continua (Continous Integration / Continous Delivery [CI/CD]) con pipelines dinámicas, a menos que realmente sea necesario. Siempre sigue el principio YAGNI ("You Aren't Gonna Need It"), del inglés: no lo vas a necesitar.
Caso de Uso
Trabajando en el equipo Serverless en Datadog, me encontré con bastantes flujos de lanzamiento que no estaban automatizados. Esto no se había hecho pues no se podía justificar colocar horas de ingeniería en algo cuyo proceso manual funcionaba perfectamente bien. Hasta que los problemas salieron – varias veces.
Déjame explicarte como crear una pipeline dinámico. Usaremos el escenario de mi equipo como referencia. Aunque lo simplificaré por el bien de este blog.
Necesitábamos compilar un proyecto en múltiples tiempos de ejecución (runtimes). Las pruebas de integración tenían que seguir esta regla también. Usar matrices paralelas no eran una opción, pues por cada N runtimes, teníamos que esperar a los N trabajos, y eso no era paralelización real del trabajo.
Creando una Pipeline Dinámica
Para crear una pipeline dinámica en GitLab, necesitas tres componentes clave: definir un trabajo cuyo único propósito sea generar la pipeline deseada; también necesitas los parámetros para usar dinamicamente; y, la plantilla que el trabajo generador utilizará.
He escogido gomplate, un proyecto renderizador de plantillas codificado en Go, que nos permitirá simplificar el proceso. Primero, definamos las entradas, salidas, y los parámetros en el archivo de configuración.
Configuración
La configuración será el punto de entrada para gomplate. Esta debe ser escrita en sintaxis YAML. Para un caso de uso sencillo, se tienen que definir inputFiles
, outputFiles
, y datasources
.
# ci/config.yaml
inputFiles:
- ci/input_files/dynamic-pipeline.yaml.tpl # aquí vivirá nuestra plantilla
outputFiles:
- ci/dynamic-pipeline.yaml # esta será la pipeline producto a ejecutar
datasources:
runtimes:
url: ci/datasources/runtimes.yaml # los runtimes deseados a ejecutar
environments:
url: ci/datasources/environments.yaml # sandbox y producción
regions:
url: ci/datasources/regions.yaml # después queremos publicar en estas regiones
Los archivos de entrada incluirán tu plantilla, o plantillas; los archivos de salida especifican el directorio de la pipeline de salida; y los parámetros (datasources) son los valores dinámicos que se usarán en la plantilla.
Datasources
Gomplate acepta múltiples datasources, pero aquí, usaremos un archivo local escrito en YAML.
# ci/datasources/runtimes.yaml
runtimes:
- name: "node16"
node_version: "16.14"
node_major_version: "16"
- name: "node18"
node_version: "18.12"
node_major_version: "18"
- name: "node20"
node_version: "20.9"
node_major_version: "20"
# ci/datasources/regions.yaml
regions:
- code: "us-east-1"
- code: "us-east-2"
- code: "us-west-1"
- code: "us-west-2"
# ci/datasources/environments.yaml
environments:
- name: sandbox
- name: prod
Plantilla
Este es el elemento más importante, pues será la base de cualquier trabajo dinámico que generemos. Una plantilla puede acceder cualquier cantidad de parámetros especificados en la configuración.
Abajo, hemos definido tres etapas (stages) que serán ejecutadas por cada entrada en los parámetros runtimes
. En la etapa publish
, se generarán dos trabajos por entorno (environments
): sandbox
y prod
. En esta misma sí usaremos una matríz paralela, pues es nuestra última etapa y nada depende de ella.
# ci/input_files/dynamic-pipeline.yaml.tpl
stages:
- build
- test
- publish
# por cada tiempo de ejecución, queremos crear múltiples trabajos
{{ range $runtime := (ds "runtimes").runtimes }}
build ({{ $runtime.name }}):
stage: build
image: node:{{ $runtime.node_major_version }}-bullseye
artifacts:
paths:
- .dist/build_node{{ $runtime.node_version }}.zip
script:
- NODE_VERSION={{ $runtime.node_version }} ./scripts/build.sh
integration-test ({{ $runtime.name }}):
stage: test
image: node:{{ $runtime.node_major_version }}-bullseye
needs:
- build ({{ $runtime.name }})
dependencies:
- build ({{ $runtime.name }})
script:
- RUNTIME_PARAM={{ $runtime.node_major_version }} ./ci/run_integration_tests.sh
# por cada entorno, crearemos dos trabajos de publicación
{{ range $environment := (ds "environments").environments }}
publish {{ $environment.name }} ({{ $runtime.name }}):
stage: publish
image: docker:20.10
needs:
- build ({{ $runtime.name }})
- integration-test ({{ $runtime.name }})
dependencies:
- build ({{ $runtime.name }})
parallel:
matrix:
- REGION: {{ range (ds "regions").regions }}
- {{ .code }}
{{- end}}
script:
- STAGE={{ $environment.name }} NODE_VERSION={{ $runtime.node_version }} ./ci/publish.sh
{{- end }} # environments range
{{- end }} # runtimes range
Trabajo Generador
Finalmente, necesitamos definir el trabajo que generará tu pipeline.
# .gitlab-ci.yml
stages:
- generate
- run
generate-pipeline:
stage: generate
image: golang:alpine # utilizaremos una imagen ligera de golang
script:
- apk add --no-cache gomplate # instalamos gomplate
- gomplate --config ci/config.yaml # ejecutamos con nuestro archivo de configuración
artifacts:
paths:
- ci/dynamic-pipeline.yaml # delegamos la pipeline al siguiente trabajo
execute-dynamic-pipeline:
stage: run
trigger:
include:
- artifact: ci/dynamic-pipeline.yaml
job: generate-pipeline
strategy: depend
Pipeline en GitLab
En nuestro caso de uso, la pipeline será ejecutada en cada merge request. La pipeline generada será una pipeline downstream que será ejecutada después de que el trabajo generador haya terminado.