Desplegar servicios
En muchas ocasiones vamos a necesitar desplegar nuestros propios proyectos, posiblemente a través de un proceso CI/CD. Esta sección es para esos casos.
Según las herramientas que utilicemos, la forma de desplegar será diferente. En este artículo se utilizan estos:
- El ejemplo se basa en el despliegue de esta web, así que los ficheros reales se pueden encontrar en el repositorio. Existen ligeras diferencias entre los explicados aquí.
- GitLab Runner es el sistema utilizado para el CI/CD, con lo que el fichero
.gitlab-ci.yaml
dispone los detalles sobre el proceso. - Helm es el sistema utilizado para el despliegue. Desde el CI/CD se generan los valores acorde a los requisitos por el clúster.
- El proyecto de GitLab está enlazado a un GitLab Runner desplegado en la misma red que el clúster, esto le permite conectar remotamente a partir de un kubeconfig. No se utilizan agentes externos.
- Las variables de CI/CD se configuran a nivel del proyecto en GitLab. Estos no son visibles en el proyecto, así que son explicados en esta documentación.
- Dentro del proceso de CI/CD se encuentra la compilación, creación del contenedor, publicación de este en Nexus Repository y su despliegue. Esta documentación únicamente explica el proceso de despliegue en Kubernetes. Si se desea explorar el resto del proceso, es preferible mirar el fichero de gitlab-ci directamente.
- El clúster está configurado acorde al K8s Project y utiliza el Nexus Repository como registro de contenedores por defecto, así que no requiere indicar el dominio donde encontrar el contenedor. El CI/CD en cambio sí lo necesita ya que GitLab Runner está desplegado sobre un Docker y este busca automáticamente en
hub.docker.com
.
Para entender todo el procedimiento, se debe tener unos conocimientos mínimos de CI/CD y del sistema creado por GitLab.
Variables
El sistema utilizado tiene dos ramas que se relacionan acorde al entorno: PROD
para la rama master
y TEST
para la rama develop
. Ya que los valores son diferentes según el entorno, estos se duplican y el proceso de CI/CD escoge la correcta según desde que rama se ejecuta el proceso.
El proceso necesita los datos de conexión con el clúster y los datos finales como la URL acorde al entorno.
Variable | Descripción | Ejemplo |
---|---|---|
KUBECONFIG_PROD | KubeConfig con un usuario con acceso a desplegar el servicio en producción | - |
KUBECONFIG_TEST | Igual que el anterior, pero con acceso a test | - |
PUBLISH_DOMAIN_PROD | Dominio utilizado en producción | domain.cat |
PUBLISH_DOMAIN_TEST | Dominio utilizado en test | domain.intranet |
PUBLISH_NAME | Nombre del subdominio donde se desplegará | www |
INTRANET | true si el servicio se despliega en intranet o false si va en extranet | false |
NAMESPACE | Namespace previamente creado y del que se tiene acceso para desplegar el servicio | documoon |
Proceso
El resultado final del proceso de despliegue es el siguiente:
k8s deploy:
image:
name: alpine/helm
entrypoint: [""]
stage: deploy
cache: []
before_script:
- apk add --update --no-cache jq
# Configure kubeconfig
- >
if [[ $CI_COMMIT_REF_NAME == "master" ]]; then
KUBECONFIG=$KUBECONFIG_PROD
else
KUBECONFIG=$KUBECONFIG_TEST
fi
script:
# Retrieve service name
- SERVICE_NAME=$(jq -r ".name" package.json)
# Prepare URL
- >
if [[ $CI_COMMIT_REF_NAME == "master" ]]; then
PUBLISH_DOMAIN=$PUBLISH_DOMAIN_PROD
else
if [ $INTRANET == "true" ]; then
PUBLISH_NAME="$PUBLISH_NAME.test"
else
PUBLISH_NAME="$PUBLISH_NAME-extranet.test"
fi
PUBLISH_DOMAIN=$PUBLISH_DOMAIN_TEST
fi
# Choose ingress classname
- >
if [ $INTRANET == "true" ]; then
INGRESS_CLASSNAME=nginx-intranet
else
INGRESS_CLASSNAME=nginx-extranet
fi
# Prepare values
- |
cat << EOF > helm-values.yaml
ingress:
enabled: true
className: $INGRESS_CLASSNAME
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
host: $PUBLISH_NAME.$PUBLISH_DOMAIN
EOF
# Change to non-stable image
- |
if [[ $CI_COMMIT_REF_NAME != "master" ]]; then
cat << EOF >> helm-values.yaml
image:
pullPolicy: Always
tag: develop
podAnnotations:
commit-sha: $CI_COMMIT_SHA
EOF
fi
# Deploy
- >
helm upgrade --install $SERVICE_NAME ./helm \
-f helm-values.yaml \
--namespace $NAMESPACE \
--kubeconfig $KUBECONFIG
only:
- master
- develop
except:
- schedules
Debido al tamaño del proceso, he preferido dividirlo y explicar cada sección por separado.
Imagen
Cada proceso de GitLab Runner inicia un contenedor donde se copia el código fuente y se ejecutan las órdenes. Debido a que se despliega a través de Helm, hace falta utilizar un contenedor diseñado para ello.
La imagen de alpine/helm
dispone de lo necesario para este caso, aunque los procesos de CI/CD deben anular el uso por defecto de entrypoint
para poder ejecutar ordenes dentro del contenedor.
image:
name: alpine/helm
entrypoint: [""]
Orden y caché
Cada proceso tiene que estar definido a un stage
para que GitLab Runner pueda ejecutarlos en orden y en paralelo cuando sea posible.
Debido a que mi proceso tiene una cache por defecto y el despliegue no la requiere, la deshabilito para que el sistema no pierda tiempo.
stage: deploy
cache: []
Preparar el contenedor
El proceso requiere de dos puntos:
- La capacidad de leer ficheros JSON para extraer el nombre del servicio (otros servicios pueden requerir XML).
- Conocer qué KubeConfig utilizar acorde al entorno.
Este punto preinstala y configura las variables requeridas para que las órdenes del proceso funcionen según lo esperado.
before_script:
- apk add --update --no-cache jq
# Configure kubeconfig
- >
if [[ $CI_COMMIT_REF_NAME == "master" ]]; then
KUBECONFIG=$KUBECONFIG_PROD
else
KUBECONFIG=$KUBECONFIG_TEST
fi
Despliegue
Este punto tiene diferentes apartados que son ejecutados en orden:
- Obtener el nombre del servicio
- Construir la URL que será utilizada por el Ingress
- La rama no
master
añade untest.
al dominio - En caso de desplegarse en extranet, la rama no
master
además añade un-extranet
en el dominio
- La rama no
- Se define la clase de Ingress acorde a la variable de intranet
- Los valores se definen en un fichero de
values
acorde a Helm para activar y configurar Ingress - Los contenedores en la rama no
master
utilizan el nombre de la rama como versión del contenedor. Hay que ajustar Helm para apuntar a la versión correcta y obligar a que el pod se vuelva a generar acorde a la nueva imagen. - Despliegue en Helm acorde a los valores creados, el namespace y el KubeConfig
script:
# Retrieve service name
- SERVICE_NAME=$(jq -r ".name" package.json)
# Prepare URL
- >
if [[ $CI_COMMIT_REF_NAME == "master" ]]; then
PUBLISH_DOMAIN=$PUBLISH_DOMAIN_PROD
else
if [ $INTRANET == "true" ]; then
PUBLISH_NAME="$PUBLISH_NAME.test"
else
PUBLISH_NAME="$PUBLISH_NAME-extranet.test"
fi
PUBLISH_DOMAIN=$PUBLISH_DOMAIN_TEST
fi
# Choose ingress classname
- >
if [ $INTRANET == "true" ]; then
INGRESS_CLASSNAME=nginx-intranet
else
INGRESS_CLASSNAME=nginx-extranet
fi
# Prepare values
- |
cat << EOF > helm-values.yaml
ingress:
enabled: true
className: $INGRESS_CLASSNAME
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
host: $PUBLISH_NAME.$PUBLISH_DOMAIN
EOF
# Change to non-stable image
- |
if [[ $CI_COMMIT_REF_NAME != "master" ]]; then
cat << EOF >> helm-values.yaml
image:
pullPolicy: Always
tag: develop
podAnnotations:
commit-sha: $CI_COMMIT_SHA
EOF
fi
# Deploy
- >
helm upgrade --install $SERVICE_NAME ./helm \
-f helm-values.yaml \
--namespace $NAMESPACE \
--kubeconfig $KUBECONFIG
Ramas para el proceso
Tras cualquier commit
recibido en el proyecto, se iniciarán todos los procesos. Esto puede limitarse.
En este caso se limita únicamente a dos ramas acorde a su nombre y se anula su ejecución mediante temporizadores.
only:
- master
- develop
except:
- schedules