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.yamldispone 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 masterañade untest.al dominio
- En caso de desplegarse en extranet, la rama no masterademás añade un-extraneten 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 valuesacorde a Helm para activar y configurar Ingress
- Los contenedores en la rama no masterutilizan 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