Blog de Amazon Web Services (AWS)

Clúster de Jenkins en Kubernetes con Cómputo Serverless.

Por Rene Martínez es Arquitecto de Soluciones Senior en AWS

 

El software libre u “open source” cobra cada vez más auge en el mundo TI, permite que personas a fin se junten y colaboren desinteresadamente en la construcción de un bien común que pueda ser utilizado libremente por todos. No sólo utilizado como fue creado, sino adoptarlo y adaptarlo a las necesidades particulares de cada persona o corporación. Esta posibilidad que brinda el “open source” promueve la innovación y libertad en nuestros clientes y estas son características que apreciamos mucho en AWS. Por este motivo, estamos comprometidos con el software libre y cientos de ingenieros de Amazon contribuyen activamente a esta comunidad a través de proyectos como: Correto, Apache Kafka, Kubernetes, OpenDistro, OpenTelemetry, entre otros .

De igual forma que el “open source”, la computación sin servidor o “serverless”, también viene cobrando mucha relevancia en nuestros tiempos. ¿Por qué motivo? Una de las razones más evidente es el precio, donde se factura sólo por el uso efectivo que tuvo el servicio a muy bajo costo, por ejemplo:

En AWS Lambda por el cómputo, desde $0.20 por millón de invocaciones.

Amazon S3 por almacenamiento, hasta $0.99 por terabyte almacenado en la capa más fría.

Aunque el precio es realmente atractivo, otras de las razones es que al usar servicios “serverless”, los clientes pueden abstraerse totalmente de gestión de infraestructura o sistema operativo, disponibilidad, escalabilidad y gran parte de la responsabilidad de seguridad de las aplicaciones.

El objetivo de este blog post es mostrarles cómo pueden combinarse estás dos tendencias asociadas al mundo de Devops. Veremos una forma desplegar una solución de CI/CD serverless basada en un clúster de Jenkins desplegado en Amazon EKS. Al usar un modo de despliegue de Jenkins clusterizado, se puede desacoplar el nodo controlador de los ejecutores corriendo los agentes por el tiempo exacto que toma la construcción, y de esta forma, aprovechar más aun las ventajas de la computación sin servidor.

 

Diseño de la solución.

Principales componentes utilizados.

Comunidad: AWS:
Kubernetes Amazon EKS
Jenkins AWS Fargate
Nginx Amazon EFS
Helm AWS Cloud9

 

Arquitectura:

 

En la siguiente publicación usaremos la región de Oregon (us-west-2) para desplegar los recursos, pueden usar cualquier otra si lo desean, pero deberán ajustar los parámetros de las URL y de los comandos proporcionados.

Como ambiente de desarrollo recomendamos el uso de AWS Cloud9, ya que provee una forma fácil y segura de conectarse a los servicios de AWS, incluye editor de código, terminal de Linux y una gran variedad de herramientas de desarrollo pre-instaladas. Para crear el ambiente, diríjase a la consola de AWS en la región preferida mediante el siguiente vínculo: https://us-west-2.console.aws.amazon.com/cloud9/home?region=us-west-2) y lance un ambiente usando los valores por defecto y una subnet pública de cualquier VPC que exista en la cuenta como se ve a continuación.

 

 

Cuando esté disponible el ambiente, debemos ejecutar los siguientes pasos para asignarle un rol con privilegios de administración y que lo use en lugar de las credenciales temporales por defecto. Esto lo hacemos así sólo para ganar agilidad en el desarrollo de los pasos de esta publicación, en ambientes productivos no es recomendable y se debería seguir la buena práctica de seguridad de otorgar el mínimo privilegio.

 

  1. Use el siguiente enlace para acceder a la interfaz de creación del rol con permisos de administración directamente: https://console.aws.amazon.com/iam/home#/roles$new?step=review&commonUseCase=EC2%2BEC2&selectedUseCase=EC2&policies=arn:aws:iam::aws:policy%2FAdministratorAccess [BL1]
  2. Confirme que efectivamente el nuevo rol que están creando tiene la política de acceso de administración y presione siguiente hasta llegar al paso del nombre.
  3. Introduzca el nombre del rol, por ejemplo: “cloud9-role” y finalice la creación.
  4. Acceda a la interfaz de gestión de Amazon EC2 mediante el acceso directo en el ambiente de Cloud9.

 

 

5. Asigne el nuevo rol recién creado

 

 

6- Regrese al ambiente de Cloud9, acceda las preferencias del ambiente, luego AWS Settings y desmarque la casilla de credenciales temporales como aparece en la siguiente imagen.

 

 

7 – Por último, ejecute los siguientes comandos para eliminar las credenciales temporales en uso y validar que se esté usando el nuevo rol que fue creado.

rm -vf ${HOME}/.aws/credentials

aws sts get-caller-identity

El resultado debe ser un json con el ARN del rol asignado en los pasos anteriores.

Con el ambiente de desarrollo operativo, instalemos las herramientas de trabajo de Kubernetes y Amazon EKS.

1 – kubectl. Detalles en la documentación oficial:  https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html

Ejecutar el comando:

curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.19.6/2021-01-05/bin/linux/amd64/kubectl && chmod +x ./kubectl

para descargar el binario y otorgar permisos de ejecución y luego el siguiente para moverlo al directorio de binarios del sistema operativo.

sudo mv /home/ec2-user/environment/kubectl /usr/local/bin

kubectl version --short –client

2 – eksctl. Detalles en la documentación pública. https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html

Ejecutar el comando:

curl --silent --location
"https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname 
-s)_amd64.tar.gz" | tar xz -C /tmp

para descargar el binario y luego los siguientes para moverlo para la carpeta local de binarios

sudo mv /tmp/eksctl /usr/local/bin

eksctl version

El resultado debería ser similar al visible en la siguiente imagen:

 

 

Una vez instalada la herramienta de línea de comando de Amazon EKS procedamos a la creación del clúster basado en AWS Fargate.

Para facilitar el trabajo a los lectores hemos preparado un repositorio con todas las configuraciones necesarias, quedando pendiente reemplazar algunos campos con los valores específico de sus ambientes, como: <ACCOUNT_ID>, <FILE_SYSTEM_ID> y <ACCESS_POINT_ID>.

Pueden descargar el repositorio directamente en el ambiente de Cloud9 mediante el siguiente comando:

git clone  https://github.com/aws-samples/serverless-jenkins-cluster-on-eks.git 
cd serverless-jenkins-cluster-on-eks

Primero usaremos el fichero cluster-fargate.yaml que contiene las configuraciones iniciales de nuestro clúster de Kubernetes, entre ellas el “fargateProfiles” que contiene el selector de “namespace” que va a determinar si un pod se ejecuta sobre AWS Fargate o Amazon EC2. En este caso no es necesario ajustar ningún valor, si desean profundizar en otras configuraciones disponibles pueden ver la documentación oficial de eksctl en su sitio web.

eksctl create cluster -f cluster-fargate.yaml

Este paso puede tomar de 10 a 15 minutos en terminar pues se van a generar todos los recursos necesarios para el clúster: redes, seguridad, almacenamiento, computo y desplegar el “control plane” de kubernetes de forma redundante en 3 zonas de disponibilidad. Para monitorear el avance de los despliegues pueden ir a AWS CloudFormation y buscar los stacks que contengan el nombre del clúster que eligieron, por defecto en el archivo viene: jenkins-eks.

Si todo sale bien deberían ver algo similar a las siguientes imágenes al inicio y final de la ejecución del comando.

 

Inicio

Fin

En este punto, ya tenemos un clúster de Amazon EKS sobre AWS Fargate. Si quieren ver el clúster y sus componentes directamente en la consola de EKS, deben mapear el rol con que están autenticados con un role de kubernetes autorizado, mas detalles en la documentación de RBAC.

eksctl create iamidentitymapping \

--cluster jenkins-eks \

--arn arn:aws:iam:: <ACCOUNT_ID>:role/<ROLE-NAME> \

--group system:masters --username admin

Nota: Si están usando un rol de AWS SSO deben eliminar la siguiente parte del ARN: “/aws-reserved/sso.amazonaws.com”. Más detalles en la documentación oficial. Más detalles en la documentación oficial.

Lo siguiente es desplegar los recursos necesarios dentro del clúster, para ello, usaremos el gestor de paquetes más popular actualmente disponible para kubernetes: Helm. Dicho gestor se graduó como proyecto en la CNCF y es mantenido por la comunidad, para instalarlo, debemos ejecutar el siguiente comando:

curl -sSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-
3 | bash

Si notan los “node groups” del clúster, dejamos uno encendido permanentemente y el mismo está basado en Amazon EC2, ahí se van a desplegar los elementos fundacionales de la solución. Elementos como: el “ingress controller“ de kubernetes, quien se encarga de exponer los servicios desplegados en el clúster hacia el exterior, el “volumen controller” para utilizar Amazon EFS y otros componentes internos asociados al namespace “kube-system”.

Para acceder a los servicios internos desplegados en el clúster, manteniendo la tendencia de usar componentes open source, usaremos el “ingress controller” de NGINX, el cual se puede instalar mediante los siguientes comandos.

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
kubectl create ns ingress
helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress

Una vez ejecutados estos comandos, podrán ver en la consola de Amazon EC2 que se creó un balanceador de tipo “Classic ”  que servirá de punto de entrada a los servicios que despleguemos más adelante. Para probar que esté todo bien vamos a desplegar un servidor http nginx básico en el namespace de jenkins para que se ejecute sobre nodos de tipo AWS Fargate. Vamos a hacerlo mediante los siguientes comandos.

kubectl create ns jenkins
kubectl run nginx --image nginx -n jenkins
kubectl expose pod nginx --port 80 -n jenkins
kubectl apply -f ingress-nginx.yaml -n jenkins

y unos segundos después, mantenerse ejecutando el comando:

kubectl describe ingress nginx-ingress -n jenkins

hasta que el valor “Address” de la respuesta contenga el valor de DNS del balanceador creado por con el “ingress controller”.

Si toman ese valor y lo pegan en una ventana del navegador deben ver la página de bienvenida del servidor http de nginx, similar a la siguiente imagen:

 

 

Una vez ejecutada la prueba podemos eliminar los recursos utilizados mediante los siguientes comandos:

kubectl delete -f ingress-nginx.yaml -n jenkins
kubectl delete pods,services nginx -n jenkins

Para que las configuraciones y pipelines de Jenkins sean persistentes, sobrevivan a reinicios y soporten eventos de escalamiento, es necesario almacenar los datos del nodo principal en un lugar externo a AWS Fargate. En Amazon EKS esto se consigue mediante los Persistent Volume Claim (PVC) y los drivers de Container Storage Interface (CSI).

Para mantener una solución de tipo “serverless” proponemos almacenar estos datos en Amazon EFS. Este servicio ofrece un sistema de archivos elástico, sencillo y altamente disponible, que permite compartir datos sin necesidad de aprovisionar o administrar infraestructura ni almacenamiento. A continuación, proveeremos los comandos necesarios para aprovisionarlo y configurarlo.

Primero necesitamos el id de la VPC donde está desplegado el clúster de EKS, pueden observarlo directamente en la consola de AWS o mediante el siguiente comando:

aws eks describe-cluster --name jenkins-eks --query
"cluster.resourcesVpcConfig.vpcId" --region us-west-2 --output text

Con la salida del comando anterior, creamos el Security Group (SG) para permitir acceso al sistema de ficheros de la siguiente forma

aws ec2 create-security-group \
--region us-west-2 \
--group-name efs-mount-sg \
--description "Amazon EFS for EKS, SG for mount target" \
--vpc-id <VPC_ID>

 

Con el SG creado debemos agregar una regla de entrada utilizando el rango de CIDR de la VPC, pueden verlo en la consola o mediante el comando:

aws ec2 describe-vpcs \
--region us-west-2 \
--vpc-ids <VPC_ID> \
--query "Vpcs[].CidrBlock" \
--output text

luego utilizando el id del SG creando anteriormente y el rango de la VPC que se acaba de obtener

aws ec2 authorize-security-group-ingress \
--region us-west-2 \
--group-id <SG_ID> \
--protocol tcp \
--port 2049 \
--cidr <CIDR>

El próximo paso es crear el sistema de ficheros como tal

aws efs create-file-system \
--creation-token creation-token \
--performance-mode generalPurpose \
--throughput-mode bursting \
--region us-west-2 \
--tags Key=Name,Value=MyEFSFileSystem \
--encrypted

Para poder acceder al sistema de ficheros desde los nodos del clúster de Amazon EKS, tenemos que crear “mount_targets”, para ello necesitamos los id de las subnets privadas donde estarán corriendo los nodos, el id del SG creado anteriormente y el id del “file-system” entregado por el comando anterior.

aws efs create-mount-target \
--file-system-id <FILE_SYSTEM_ID> \
--subnet-id <SUBNET_A_ID> \
--security-group <SG_ID> \
--region us-west-2

Deben repetir este comando con todas las subnets privadas de la VPC del clúster de Amazon EKS. Deberían ser tres en total si usaron la región de Oregon.

Cuando se monta el sistema de archivos de Amazon EFS por defecto, solo el usuario “root” del sistema operativo tiene permisos para leer o escribir. Si recuerdan, en Amazon EKS sobre AWS Fargate no se pueden ejecutar contenedores en modo “privileged”, por lo tanto, debemos crear un “access point” de Amazon EFS para que el usuario “jenkins”, configurado como 1000 en el chart de Helm, pueda leer y escribir ficheros en el volumen a mapear en el contenedor dentro del pod.

aws efs create-access-point \
--file-system-id <FILE_SYSTEM_ID>\
--posix-user Uid=1000,Gid=1000 \
--root-directory "Path=/jenkins,CreationInfo={OwnerUid=1000,OwnerGid=1000,Permissions=777}" \
--region us-west-2

La seguridad siempre ha sida la máxima prioridad para AWS, por ello, no es suficiente autorizar el sistema operativo del contenedor y el tráfico por la red hacia el sistema de ficheros, también necesitamos un rol de IAM con los permisos necesarios. Dicho rol debe estar mapeado con un ServiceAccount dentro del clúster de kubernetes. Podemos hacer esto de forma automática mediante el uso de eksctl y los siguientes comandos:

aws iam create-policy \
--policy-name AmazonEKS_EFS_CSI_Driver_Policy \
--policy-document file://iam-policy-example.json

para crear la política,

eksctl utils associate-iam-oidc-provider --region=us-west-2 --
cluster=jenkins-eks –approve

para aprobar a IAM como un proveedor de identidad (IDP) para OpenID Connect en el clúster y

eksctl create iamserviceaccount \
--name efs-csi-controller-sa \
--namespace kube-system \
--cluster serverless-jenkins \
--attach-policy-arn arn:aws:iam:: <ACCOUNT_ID>:policy/AmazonEKS_EFS_CSI_Driver_Policy \
--approve \
--region us-west-2

Para crear el ServiceAccount, un rol de IAM y el “binding” en kubernetes para mapear uno con el otro.

Para finalizar la parte del sistema de ficheros necesitamos instalar el driver de CSI para Amazon EFS. En este caso usaremos Helm nuevamente para facilitar el proceso y no va a ser necesario modificar ningún valor en los comandos.

helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/
helm repo update
helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
--namespace kube-system \
--set image.repository=602401143452.dkr.ecr.us-west-2.amazonaws.com/eks/aws-efs-csi-driver \
--set serviceAccount.controller.create=false \
--set serviceAccount.controller.name=efs-csi-controller-sa

Pocos segundos después deberían ver un DeamonSet y un Deployment de kubernetes asociado al driver de CSI como se muestra en la siguiente imagen.

 

 

Con el controlador y el sistema de ficheros desplegados solo nos queda crear el “Persistent Volume Claim” como tal, para ello, debemos reemplazar los valores necesarios del fichero pvc.yaml y ejecutar el siguiente comando:

 

kubectl apply -f pvc.yaml -n jenkins

Adicionalmente al chequeo anterior en la interfaz visual de “workloads” en la consola de EKS, pueden regresar al ambiente de Cloud9 y ejecutar los siguientes comandos:

kubectl get pods -n ingress
kubectl get pvc -n jenkins
kubectl get serviceaccount efs-csi-controller-sa -n kube-system
kubectl get pods -n kube-system | grep csi

El resultado debería ser similar al siguiente:

 

 

Si todo está correcto y “running”, finalmente llegó el momento esperado, es hora de desplegar el clúster de Jenkins. En el repositorio que descargaron al inicio hay un “chart” de Helm que ya está configurado con los plugins y parámetros necesarios para que funcione en al ambiente que hemos ido preparando hasta ahora. En cada sección de configuración vienen comentarios explicando que se consigue con los valores asignados. Para desplegarlo debemos usar los siguientes comandos:

helm repo add jenkins https://charts.jenkins.io
helm repo update
helm install jenkins jenkins/jenkins -f jenkins-values.yaml -n jenkins

Podemos validar que la auto-configuración de Jenkins fue ejecutada correctamente mediante la ejecución del siguiente comando:

kubectl logs jenkins-0 -c init -n jenkins

Y validar que muestre la siguiente salida:

 

 

Si aún no les muestra nada, deben esperar unos segundos más a que el nodo de AWS Fargate donde va a correr el pod controlador de Jenkins esté registrado en el clúster kubernetes.

Podemos esperar unos 3 minutos más para ver los logs del nodo controlador de Jenkins través de este otro comando:

kubectl logs jenkins-0 -c jenkins -n jenkins

El cual debería tener algo similar a la siguiente imagen al final de la salida. Entre todas las entradas de tipo INFO y WARNING la importante es la que está señalada.

 

 

De forma similar que se hizo el mapeo de un ServiceAccount para acceder al sistema de ficheros de Amazon EFS, es necesario hacerlo para que Jenkins pueda hacer solicitudes a Amazon EKS de iniciar nodos de AWS Fargate donde se van a desplegar los nodos ejecutores de los pipelines.

kubectl create clusterrolebinding jenkinsrolebinding --clusterrole=cluster-
admin --group=system:serviceaccounts:jenkins

Y por último desplegar el elemento de tipo ingress en el clúster:

kubectl apply -f ingress-jenkins.yaml -n jenkins

Para acceder a la interfaz web de Jenkins y hacer las pruebas finales se debe obtener la contraseña del usuario “admin”.

printf $(kubectl get secret --namespace jenkins jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

tal cual sugiere en el output del chart de Helm.

En este punto, ya tenemos un clúster de Jenkins totalmente serverless desplegado sobre Amazon EKS. Para comprobar que efectivamente tanto el controlador como los agentes de construcción se ejecuten sobre AWS Fargate vamos a acceder a Jenkins utilizando el usuario: “admin”, la contraseña mostrada por comando anterior y la url del “ingres” que se obtiene mediante el comando:

kubectl describe ingress jenkins-ingress -n jenkins

Una vez autenticados en Jenkins iremos a construir un “job” básico para comprobar que el “executor” funcione bien y se ejecute de forma “serverless”.

 

 

En la siguiente pantalla se asigna el nombre y el tipo “job” como “Freestyle. En la configuración de los pasos de la tarea de construcción agregamos un “build step” de tipo “execute shell” como se ve en la imagen a continuación.

 

 

Cuando el trabajo queda guardado mandamos a ejecutar una construcción de prueba mediante el botón: “Build Now” del menú de la izquierda.

Para comprobar que el “build” se ejecutó satisfactoriamente y sobre AWS Fargate podemos revisar en varios lugares.

  1. En los nodos de Jenkins, específicamente en la página de eventos como ven a continuación

 

 

2. En la interfaz de Amazon EKS en la pestaña: “Overview”. Ahí verán un nodo de Fargate con mayor tiempo de creación que es el del nodo controlador y otro nuevo que es donde se está ejecutando la construcción como tal.

 

 

3. En la consola de salida de la ejecución del “job”. Si no les da tiempo a capturar todo pueden aumentar el tiempo en el comando “sleep” del “shell script” en el paso de construcción de la tarea de ejemplo.

 

 

De esta forma, se consigue desplegar un clúster de Jenkins totalmente “serverless”, escalable, altamente disponible, persistente y costo efectivo.

¿Qué más pueden hacer?

  1. Escribir todos los pasos ejecutados en esta publicación usando infraestructura cómo código mediante el uso de CloudFormation u otra herramienta a fin.
  2. Instalar otros plugins de Jenkins según sea necesario, por ejemplo: Blue Ocean para la construcción de pipelines de múltiples ramas con visualizaciones sofisticadas.
  3. Para desplegar esta solución en un clúster privado hay que ejecutar algunos pasos extras que involucra la creación de algunos VPC Endpoints, Gateway Endpoins y modificar el coredns de kubernetes.
  4. Asociar las reglas de ingress con “host” adicionalmente a los “path”, eso es para poder apuntar nombres de dominio al servicio de Jenkins, ejemplo: jenkins.anycompany.com.
  5. Configurar y desplegar distintos tipos de agentes de Jenkins adicionales a los comunes basado en linux. Ejemplo basado en Windows containers para aplicaciones construidas con el framework .net, aplicaciones “mobile” nativas en IOS, mediante el uso de las instancias MAC de AWS.
  6. Integración privada con instalaciones propias u “on-premises” para ambientes de desarrollo altamente regulado donde el código fuente o los datos de pruebas deban cumplir con algún requisito regulatorio de residencia de datos.

 

 


Sobre el autor

Rene Martínez es arquitecto de soluciones senior en AWS con 13 años de experiencia profesional en el rubro TI y más de 5 con tecnologías de nube específicamente. En su rol actual apoya a los clientes a encontrar las mejores arquitecturas y soluciones para sus necesidades, en especial, clientes del segmento Enterprise e industria financiera.

 

 

Revisores

Bruno Laurenti es arquitecto de soluciones en AWS con foco en clientes del segmento Enterprise Financiero y Retail.

 

 

 

 

 

Jesús Federico es un Principal Solutions Architect de AWS en la vertical de telecomunicaciones que trabaja brindando orientación y asistencia técnica a los proveedores de servicios de comunicaciones en su viaje a la nube.