Creando nuestro CI/CD con Github Actions y Okteto.

Creando nuestro CI/CD con Github Actions y Okteto.

Un pipeline, completo

Muchas veces me consultan sobre pipelines, Docker o Kubernetes. Siempre he creído que el Hands On es la mejor forma de aprender, es por ello que vamos hacer una pequeña aplicación con Python & Flask, dockerizarla y luego hacer el deployment en un cluster Kubernetes. Vamos a utilizar servicios gratuitos como GitHub Actions, para la creacion de nuestro Pipeline & Okteto, para soportar nuestro deployment de Kubernetes.

Aca encontraran el proyecto y todo lo necesario para la implementación.

Lo primero que haremos será crear la imagen, de Docker, que subiremos a Docker Hub para que sea pulleada por nuestro deployment de Kubernetes. Es necesario que ingresemos las credenciales para poder hacer el push a Docker Hub y el Config File del Cluster de Kubernetes. Cada vez que actualicemos nuestro proyecto, en GitHub, correra nuestro YAML en GitHub Actions, que realizara todos estos pasos en nuestro Pipeline.

Creación de la Imagen Docker

Vamos a revisar los archivos que son necesarios, para la creación de nuestra imagen.

.
├── Dockerfile
├── app.py
├── static
│   └── style.css
└── template
    └── index.html

2 directories, 5 files

Vamos a crear la imagen, con docker build -t flask –no-cache=true ., para probarla localmente. Para ello es necesario el Dockerfile, que tenemos en nuestro proyecto.

 /tmp/Okteto  main !8  docker build -t flask --no-cache=true .   ok  4s
[+] Building 19.7s (12/12) FINISHED
 => [internal] load .dockerignore                                   0.1s
 => => transferring context: 2B                                     0.0s
 => [internal] load build definition from Dockerfile                0.1s
 => => transferring dockerfile: 496B                                0.0s
 => [internal] load metadata for docker.io/library/python:alpine    2.3s
 => [1/7] FROM docker.io/library/python:alpine@sha256:4228f7566ffd  0.0s
 => [internal] load build context                                   0.1s
 => => transferring context: 816B                                   0.0s
 => CACHED [2/7] WORKDIR /usr/src/app                               0.0s
 => [3/7] RUN pip install --upgrade pip                             8.1s
 => [4/7] RUN pip install --no-cache-dir flask flask-api ifaddr     8.6s
 => [5/7] COPY app.py ./                                            0.0s
 => [6/7] ADD static ./static                                       0.0s
 => [7/7] ADD template ./template                                   0.1s
 => exporting to image                                              0.4s
 => => exporting layers                                             0.4s
 => => writing image sha256:0af8866c65c1eff632e6ed2a3ad9e932344000  0.0s
 => => naming to docker.io/library/flask                            0.0s
 /tmp/Okteto  main !8

Corremos el Docker

Revisamos si tenemos la imagen creada, con docker images, y luego corremos los comando para crear el contenedor en base dicha imagen con docker run --publish 80:80 flask .

 /tmp/Okteto  main !8  docker images                                                                        ok
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
flask               latest              0af8866c65c1        About a minute ago   56.8MB
 /tmp/Okteto  main !8  docker run  --publish 80:80 flask                                                                    ok
2a4609ac3706
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 294-411-388

Ahora podemos probar si esta corriendo nuestro contenedor en localhost

Creación del YAML para que corra nuestro Pipeline

Podemos crear la carpeta .github/workflows/NOMBRE.YAML o hacerlo desde el proyecto, en GitHub.

Vamos a ver la primer parte donde realizamos el build & push a Dockerhub. En mi caso lo llame push.yaml. Van a encontrarlo en la ruta que comente anteriormente.

on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: Publish to Registry
      uses: elgohr/Publish-Docker-Github-Action@master
      with:
        name: safernandez666/okteto
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
        dockerfile: ./Dockerfile
        tags: "latest,${{ github.sha }}"

Sera necesario agregar los secretos, para conectarnos a Dockerhub y poder realizar el Push de nuestra imagen creada. Como pueden ver le agrego un tag que representa este build en particular y latest.

Agregamos los Secretos

Para agregar las variables de enterno lo haremos en Settings | Secrets.

Como ven no solo agregue DOCKER_USERNAME & DOCKER_PASSWORD para conectarme a Dockerhub, si no que tambien agregue el archivo de configuracion KUBE_CONFIG_DATA de Okteto, para obtener el valor de esa variable tenemos que descargar el archivo de configuración.

Obtenemos el variable para cargarla en GitHub.

cat okteto-kube.config | base64

Ahora si, tenemos casi todo listo. Vamos a revisar el push.yaml, completo, que realiza todos los pasos del pipeline.

YAML GitHub Actions

name: Publish To Docker & Okteto
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: Publish to Registry
      uses: elgohr/Publish-Docker-Github-Action@master
      with:
        name: safernandez666/okteto
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
        dockerfile: ./Dockerfile
        tags: "latest,${{ github.sha }}"

    - name: Deploy to Cluster
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
      with:
        args: set image --record deployment/nginx-deployment nginx=safernandez666/okteto:${{ github.sha }}

    - name: Verify Deployment
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
        KUBECTL_VERSION: "1.15"
      with:
        args: '"rollout status deployment/nginx-deployment"'

En este YAML vemos como hacemos el build & push y como se hace el deployment de kubernetes. El argumento mas importante es set image --record deployment/nginx-deployment nginx=safernandez666/okteto:${{ github.sha }} donde en cada push se refenciara la imagen creada. El primer deployment es necesario hacerlo a mano, para que se cree en Okteto. Podriamos crear la logica para consultar si esta creado y en el caso de no estarlo crearlo, pero para los fines practicos no suma. ¡Vamos a crearlo!

Configuración del Contexto

export KUBECONFIG=$HOME/Downloads/okteto-kube.config:${KUBECONFIG:-$HOME/.kube/config}

kubectl apply -f ./deployment/deployment.yaml

Vamos a revisar el archivo deployment.yaml donde tenemos del deploy y el servicio. Es muy sencillo, no les va costar comprenderlo, donde creamos 3 replicas de nuestra imagen y sera balanceada.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: safernandez666/okteto:latest 
        ports:
        - containerPort: 80
        imagePullPolicy: "Always"
---

apiVersion: v1
kind: Service
metadata:
  name: nginx-deployment
spec:
  type: LoadBalancer 
  ports:
    - name: http
      port: 80
  selector:
    app: nginx

Push a GitHub

Vamos a modificar algo del index.html, para probar, y hacer el push necesario para que corra nuestro pipeline.

git add .
git commit -m "Flask App"
git push origin main

Ya tenemos nuestro pipeline corriendo.

De esta manera ya tenemos nuestra imagen en Docker Hub, que luego sera pulleada por el deployment. Vamos a revisar en Okteto el nombre de nuestro Endpoint.

Podemos visitar el Endpoint y probar el balanceo a nuestros Pods. Aca les muestro como responden, via curl. Como resultado vamos a obtener en el HTML los Id de los Dockers.

Espero que les haya gustado y les sirva para crear su CI / CD, completo, gratis. ¡A disposición por cualquier consulta! En la proxima vamos a agregarle seguridad a nuestro Pipeline. Por ejemplo agregar una revision de la imager docker que hemos generado.