Deploy
¿Dónde podríamos poner este proyecto para ejecutarse? Ni da para listar la cantidad de métodos que podríamos utilizar para disponibilizar este proyecto.
Ya construimos la imagen, ahora solo es cogerla y ponerla para ejecutarse en el lugar correcto en el último stage (deploy) del pipeline.
Para hacer esto con dos métodos diferentes vamos a hacer lo siguiente:
- En develop (rama develop) vamos a alojar nuestro código en Netlify.
- En production (rama main) vamos a desplegar un contenedor en Kubernetes.
En el caso de Netlify podemos usar el netlify-cli para interactuar con la herramienta. Si observas bien la documentación tenemos todo el proceso de instalación y vamos a utilizarlo en el job de deploy.
Si no tienes una, crea una cuenta y añade un nuevo proyecto de forma MANUAL. Haz el upload de la carpeta build/ del proyecto solo para generar un nuevo sitio. Si no tienes la carpeta build solo ejecuta los mismos comandos del job build (npm run build) que la carpeta aparecerá o coge un build de algún artefacto del pipeline.
Al crear el proyecto, edita el nombre para que la URL generada por Netlify quede más amigable. Netlify usa el nombre del proyecto para crear el enlace con el dominio .netlify.app. En mi caso, alteré para curso-gitlab-ci.

Aquí ya tenemos el sitio manualmente desplegado. Lo que nos interesa en generar este proyecto es tener algunas informaciones para que podamos hacer login con la herramienta netlify-cli.

Necesitamos el Project ID, que conseguimos en configuraciones del proyecto, y de un token. En tu cuenta en https://app.netlify.com/user/applications#content genera un token y pon el tiempo de expiración de acuerdo con tu necesidad.

Vamos a crear las variables en el repositorio para el environment develop. El netlify-cli buscará las variables de ambiente NETLIFY_AUTH_TOKEN y NETLIFY_SITE_ID para el comando netlify status.

No definimos nada todavía en production, solo development hará el deploy de esta manera usando netlify.
Voy haciendo una pequeña alteración en el código solo para ver alguna diferencia.
<p className="read-the-docs">
Created by Valentin Despa and modified by David Puziol.
</p>
Nuestro job de deploy será de la siguiente manera.
deploy-dev:
stage: deploy
needs: [build] # Dependemos del build pues generará el directorio build
environment: # Las variables de develop estarán disponibles en este job.
name: develop
variables:
GIT_STRATEGY: none # No necesitamos hacer el clone del repositorio. ¿Vamos a usar el código para algo?
cache:
key: netlify-cli-cache
paths:
- ~/.npm # Cache global de npm #
before_script:
- echo "Instalando netlify-cli"
- npm install -g netlify-[email protected] # Siempre bueno fijar la versión
- netlify --version
- apk add curl
script: | # Todo esto de abajo se considera un único ítem de la lista. Podríamos haber hecho varios ítems. Lo hice de esta manera para mostrar cómo queda en el log.
echo "Verificando el estado usando las credenciales definidas en la variable de entorno."
echo $NETLIFY_AUTH_TOKEN
echo $NETLIFY_SITE_ID
netlify status
echo "Subiendo el directorio build para el proyecto $NETLIFY_SITE_ID"
netlify deploy --prod --dir build
curl 'https://curso-gitlab-ci.netlify.app'
rules:
- if: '$CI_COMMIT_BRANCH == "develop"' # Vamos a hacer este tipo de deploy solo en develop.
La imagen del job de arriba es la misma usada en default (node:22-alpine) y no utilizando extends para nada. Ni necesitamos que este runner tenga el código del repositorio, solo el artefacto con la carpeta build/. Definida la variable GIT_STRATEGY como none ya ganamos un poco de rendimiento.
Otro detalle es que cuando pasamos --prod en el comando netlify vamos a alterar el sitio público. Como aquí estamos haciendo deploys de develop en una herramienta y production en otra, no vamos a preocuparnos con eso.
Vamos a crear un merge con las modificaciones y aceptar. El deploy en dev correrá junto con la creación de la imagen, pues no estamos usando la imagen a pesar de que está siendo generada en el pipeline. La única dependencia que colocamos es el proceso de build y no el de la imagen.
Si usaste el cache correctamente, aunque hayamos cambiado el código no cambiamos las dependencias entonces debería ser utilizado en el job del build. Solo no será si no fue ejecutado ninguna vez el build anteriormente para este job.
El curl al final es solo para ver si el sitio está respondiendo y nada más. No conseguimos garantizar el cambio de un proyecto entero solo analizando la primera página.

Existen etapas de pipeline que llamamos smoke test que sirve para verificar los cambios.
El pipeline se monta de acuerdo con la estrategia. La estrategia aquí fue que desarrollo y producción están en ramas separadas, pero es posible tener solo la rama main y colocar diferentes jobs para que continúen desplegando en otros entornos (Deploy dev >>>> deploy prod). Generalmente cuando esta estrategia está montada el job que hace el deploy en producción solemos ponerlo MANUAL, es decir, solo si alguien presiona el play se ejecutará.
Para producción vamos a desplegar en kubernetes para mostrar una funcionalidad de GitLab. Algunos detalles antes de comenzar. Si estás haciendo este curso y no tienes un cluster kubernetes local para jugar recomiendo usar kind. Si no tienes docker también entonces ten en cuenta que kind depende de docker.
Script rápido para instalar las herramientas en ubuntu.
# Instala Docker y habilita para no necesitar usar sudo
curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker $USER && newgrp docker
# Instala Kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 && chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind
# Crea el cluster con un solo node master para economizar recurso
kind create cluster
# Instala kubectl para acceder al cluster
curl -LO "https://dl.k8s.io/release/$(curl -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
&& chmod +x kubectl \
&& sudo mv kubectl /usr/local/bin/kubectl
# Prueba si kubectl está comunicándose.
kubectl get nodes
# Instalando helm que vamos a necesitar
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
- El cluster que voy a desplegar no tiene el kube-api expuesto y no tiene load balancer.
- Es un cluster local en mi máquina personal.
- La tag de la imagen siempre será latest y no estamos enfocando en generar tags diferentes aquí.
- No vamos a crear manifiestos kubernetes, solo aplicar directo lo que no es muy correcto de hacer.
- Vamos a instalar el GitLab kubernetes Agent en el cluster. Si la API de Kubernetes no está expuesta, entonces GitLab CI no consigue acceder al cluster directamente con kubeconfig, pero el agent dentro del cluster consigue y es más seguro. Si no vas a utilizar GitOps esta es una de las mejores maneras de hacerlo.
Instalando el GitLab K8S Agent
En la página del proyecto, en Operate >>> Kubernetes clusters podemos conectar GitLab a un cluster.

Vamos a crear gitlab-agent aquí para nuestro cluster. Voy a llamarlo localcluster. Este cluster será usado solo para nuestra rama main simulando nuestro entorno productivo.

Nos dará un comando para ejecutar para hacer la instalación del agente en el cluster vía helm, incluso con el token completado.
helm repo add gitlab https://charts.gitlab.io
helm repo update
helm upgrade --install localcluster gitlab/gitlab-agent \
--namespace gitlab-agent-localcluster \
--create-namespace \
--set config.token=glagent-tnRaoRCrXxkK726CDB7zX-XXXXXXXXXXXXXXXXXXXXXX \
--set config.kasAddress=wss://kas.gitlab.com
Verificando lo que tenemos de pods en el cluster en el namespace que creamos
❯ k get pods -n gitlab-agent-localcluster
NAME READY STATUS RESTARTS AGE
localcluster-gitlab-agent-v2-676cb8cfb9-mjpwn 1/1 Running 0 3m39s
localcluster-gitlab-agent-v2-676cb8cfb9-ntv4t 1/1 Running 0 3m39s
Ahora vamos a crear el job en cicd/deploy/deploy.yaml para ejecutar un deploy en este cluster cuando llegue a main.
deploy-prod:
stage: deploy
when: manual # lo puse solo para mejorar los conceptos, pero podría ser todo automatizado.
variables:
GIT_STRATEGY: none # No vamos a necesitar el código entonces vamos a ganar velocidad
KUBE_CONTEXT: puziol/learn-gitlab-app:localcluster # Creamos esta variable que podría estar configurada dentro de un environment production
image:
name: bitnami/kubectl:latest
entrypoint: [""]
script:
- |
echo "Creando deployment con la imagen davidpuziol/learn-gitlab-app:latest"
kubectl config get-contexts
kubectl config use-context $KUBE_CONTEXT
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: learn-app-deployment
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: learn-app
template:
metadata:
labels:
app: learn-app
spec:
containers:
- name: learn-app
image: davidpuziol/learn-gitlab-app:latest
imagePullPolicy: Always
ports:
- containerPort: 3000
EOF
echo "Creando service para exponer la aplicación en el puerto 3000"
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: learn-app-service
namespace: default
spec:
selector:
app: learn-app
ports:
- protocol: TCP
port: 3000
targetPort: 3000
type: ClusterIP
EOF
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Si este código estuviera en el repositorio.
deploy-prod:
stage: deploy
when: manual
variables:
## GIT_STRATEGY: none # Necesitaríamos hacer el clone
KUBE_CONTEXT: puziol/learn-gitlab-app:localcluster
image:
name: bitnami/kubectl:latest
entrypoint: [""]
script:
- |
echo "Creando deployment con la imagen davidpuziol/learn-gitlab-app:latest"
kubectl config get-contexts
kubectl config use-context $KUBE_CONTEXT
kubectl apply k8s/deployment.yaml
kubectl apply k8s/service.yaml
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Para que funcione es necesario crear un merge request para main. Este mismo repositorio podría conectarse en varios clusters y por eso necesitamos avisar cuál es el context que kubectl debe usar pasando el grupo/proyecto/nombre_de_la_conexión.
Otro punto importante es que el stage deploy vendrá después del stage build y como el último job del stage build hará el push entonces necesitaremos esperar todo el proceso de build acontecer y usar needs para ganar velocidad no es posible.
Al aceptar el merge de develop para main tenemos entonces deploy-prod en espera manual.
Presionando el play vamos a hacer el deploy en kubernetes.