12

Cloudflare Pages: Cómo construimos una librería para hacer despliegues

15872Puntos

hace un año

En el vertiginoso mundo del desarrollo de aplicaciones web, contar con un proceso de CI/CD que cubra a cabalidad las necesidades de la empresa es vital. Recientemente, en el equipo de Infraestructura de Platzi, incorporamos a nuestros flujos de CI/CD el despliegue de aplicaciones a Pages, parte del Serverless Platform de Cloudflare.

Pero ejem ejem… con desplegar me refiero a desplegarla a nuestra manera, con todos los ajustes y configuraciones que nuestros procesos internos requieren, lo cual a veces se complica cuando eres early adopter como nosotros con algunos productos de Cloudflare. Mi nombre es Fernando Muñoz, Cloud Engineer en Platzi y será un placer contarte como resolvimos esta travesía.

¿Qué es Cloudflare Pages?

Cloudflare Pages es un servicio de alojamiento web y despliegue de aplicaciones (Frontend), diseñado para facilitar el proceso de despliegue desde repositorios de código, como GitHub.

Cuento corto:
Untitled.png

Cloudflare Pages se conecta al repositorio en GitHub y comienza a hacer despliegues por cada rama activa. Sin tener que preocuparse por el servidor donde va a vivir la aplicación, es decir, esto corre Serverless.

Simple, pero (siempre hay un pero)…

¿En teoría, simple no? Plug n play y listo, pero… al inicio dije que ocupamos desplegarla a nuestra manera y nuestra manera no conllevaba algo tan simple, necesitábamos:

  • Tener multistaging.
  • Manejar secretos independientes.
  • Tener custom domain por cada nuevo proyecto (producción y cada staging).
  • Manejar DNS.
  • Etc…

¿Qué es multistaging?

Staging se refiere a un entorno o ambiente separado y aislado que los devs utilizan para probar, preparar y revisar cambios antes de que se desplieguen en producción.

Untitled (5).png

Con multistaging se tiene la capacidad de generar N cantidad de staging al mismo tiempo, los cuales toman unas configuraciones base, pero luego en el lugar donde habitan, se pueden ir cambiando parámetros, configuraciones, código y todo lo que el dev imagine, sin afectar a ningún otro staging.

Varios devs pueden trabajar en una misma app al mismo tiempo y ver sus cambios en un único lugar, donde vive su aplicación sin preocuparse de afectar el trabajo, de alguien más. Luego esos cambios se pueden mergear a una rama principal y los demás staging aún activos, pueden actualizarse para tomar esos nuevos cambios base y así mantener siempre el código principal de la aplicación.

Para poder lograr y combinar todo, necesitamos algo como esto:

Untitled (1).png

Sí… lo sé…

Untitled (2).png

¿Por qué no usamos Wrangler para todo?

Pero bueno… ¿Qué opciones teníamos? Lo primero en lo que pensamos es en usar Wrangler, la herramienta de línea de comandos (CLI) proporcionada por Cloudflare para facilitar el proceso de desarrollo y despliegue de aplicaciones web en Cloudflare Workers (aaahhh los Workers… bueno son historia para otro blog) y Cloudflare Pages.

Y pensábamos que íbamos a tener un “mango bajito” al ejecutar wrangler pages -h y ver las opciones disponibles.

CleanShot 2023-07-27 at 8.29.23.png

Si bien este CLI nos permitía un par de configuraciones que necesitábamos, aun así no teníamos a mano el poder personalizar todo lo que ocupábamos.

Por ejemplo:

  • wrangler pages project create nos permite crear el proyecto en CF Pages, pero no nos dejaba configurar variables de entorno para producción o para staging. Tampoco configurarle unos “Compatibility date” y “Compatibility flags” que los proyectos necesitan.
  • wrangler pages deploy si nos resolvía el problema de subir los asset de la aplicación, pero bueno, este era el último de los problemas en la lista.
  • Entre el medio de los dos puntos anteriores, había un montón de particularidades que necesitamos poder manejar a nuestro gusto.

Nuestra opción final: La API de Cloudflare

Nuestra opción final era utilizar la API de Cloudflare, para tener mayor oportunidad de poder configurar y conectar con otros flujos que nos interesaban estuvieran dentro de estos despliegues.

Por ejemplo, teníamos que consumir el endpoint “Create project” de Cloudflare:
https://api.cloudflare.com/client/v4**/accounts/{account_identifier}/pages/projects**

Cuyo request sample se ve así:

curl --request POST \
  --url https://api.cloudflare.com/client/v4/accounts/account_identifier/pages/projects \
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email:' \
  --data '{
  "build_config": {
    "build_command":"npm run build",
    "destination_dir":"build",
    "root_dir":"/",
    "web_analytics_tag":"AQUI_VA_ALGO",
    "web_analytics_token":"AQUI_VA_ALGO"
  },
  "canonical_deployment": null,
  "deployment_configs": {
    "preview": {
      "analytics_engine_datasets": {
        "ANALYTICS_ENGINE_BINDING": {
          "dataset":"api_analytics"
        }
      },
      "compatibility_date":"2022-01-01",
      "compatibility_flags": [
        "url_standard"
      ],
      "d1_databases": {
        "D1_BINDING": {
          "id":"AQUI_VA_ALGO"
        }
      },
      "durable_object_namespaces": {
        "DO_BINDING": {
          "namespace_id":"AQUI_VA_ALGO"
        }
      },
      "env_vars": {
        "ENVIRONMENT_VARIABLE": {
          "type":"plain_text",
          "value":"hello world"
        }
      },
      "kv_namespaces": {
        "KV_BINDING": {
          "namespace_id":"AQUI_VA_ALGO"
        }
      },
      "placement": {
        "mode":"smart"
      },
      "queue_producers": {
        "QUEUE_PRODUCER_BINDING": {
          "name":"some-queue"
        }
      },
      "r2_buckets": {
        "R2_BINDING": {
          "name":"some-bucket"
        }
      },
      "service_bindings": {
        "SERVICE_BINDING": {
          "environment":"production",
          "service":"example-worker"
        }
      }
    },
    "production": {
      "analytics_engine_datasets": {
        "ANALYTICS_ENGINE_BINDING": {
          "dataset":"api_analytics"
        }
      },
      "compatibility_date":"2022-01-01",
      "compatibility_flags": [
        "url_standard"
      ],
      "d1_databases": {
        "D1_BINDING": {
          "id":"AQUI_VA_ALGO"
        }
      },
      "durable_object_namespaces": {
        "DO_BINDING": {
          "namespace_id":"AQUI_VA_ALGO"
        }
      },
      "env_vars": {
        "ENVIRONMENT_VARIABLE": {
          "type":"plain_text",
          "value":"hello world"
        }
      },
      "kv_namespaces": {
        "KV_BINDING": {
          "namespace_id":"AQUI_VA_ALGO"
        }
      },
      "placement": {
        "mode":"smart"
      },
      "queue_producers": {
        "QUEUE_PRODUCER_BINDING": {
          "name":"some-queue"
        }
      },
      "r2_buckets": {
        "R2_BINDING": {
          "name":"some-bucket"
        }
      },
      "service_bindings": {
        "SERVICE_BINDING": {
          "environment":"production",
          "service":"example-worker"
        }
      }
    }
  },
  "latest_deployment": null,
  "name":"NextJS Blog",
  "production_branch":"main"
}'

¡Dijimos, BINGO! Había mucha tela que cortar con ese endpoint, muchas opciones que podíamos entrelazar. Y con los demás endpoints íbamos a poder manejar completamente el ciclo de vida del proyecto en CF Pages:

Pero, ¿te cuento algo? Incluso la documentación de la API, no tenía todo lo que ocupábamos, faltaban parámetros o no era tan clara de como consumir los endpoints, por un momento… por un breve momento (no fue tan breve) sucumbimos al pánico.

Es decir, la documentación oficial de la API no tenía todo lo que necesitábamos saber, pero sabíamos que era posible de hacerse porque desde el dashboard de Cloudflare era posible hacer todo lo que necesitábamos.

¿Qué se hace en una situación donde ni la API ni Wrangler parecían viables?

Ir al origen, a los más recónditos e inesperados lugares, nos fuimos a ver el código de como estaba construida la API para entender y ver todos los endpoints a detalle (y bueno, unas cuantas consultas en el Discord de Cloudflare también ayudaron).

Así fue como llegamos a:
https://github.com/cloudflare/cloudflare-go/blob/master/pages_project.go

Y al inicio quedamos así:

Untitled (3).png

Pero nada que un buen focus time y un segundo turno (chiste interno de infra) no pudiera resolver.

En este punto tal vez estarás pensando… bueno, si era tan complejo, ¿por qué moverse a CF Pages? Y la respuesta es simple: todo sea por nuestros Frontends 💚. Si desde Infra lográbamos desbloquear esto, íbamos a entregarle a nuestro Frontends un nuevo y poderoso Developer Experience (DevExp).

Ya que íbamos a tener un proceso más transparente para los devs (sin nada de k8s, a.k.a kubernetes, y esas cosas raras que nos gusta a los de Infra) y los despliegues iban a poder ser muchísimo más constantes (si no pregúntenle al equipo que está desarrollando The New-Platzi). En resumen era un ganar-ganar y teníamos que desbloquear este superpoder sí o sí.

¿Cómo construímos nuestra propia librería?

Construimos nuestra propia librería para crear un CLI que hiciera a cabalidad todo lo que necesitábamos y que de paso todo lo que generara fuera compatible con los sistemas que ya tenemos en k8s y otros servicios.

La librería principalmente sirve para empaquetar todo lo que se debe de hacer para cada uno de los pasos en el ciclo de vida del proyecto (crear, customizar, actualizar, eliminar), pero sin la necesidad de estar haciendo invocaciones gigantescas como la del curl que mostré anteriormente.

La idea era pasar de tener que hacer un:

curl --request POST \
  --url https://api.cloudflare.com/client/v4/accounts/account_identifier/pages/projects \
TODO LO QUE FALTA

A simplemente hacer un:

pyflaredeployer create-project PARAMS
pyflaredeployer update-project PARAMS
pyflaredeployer delete-project PARAMS

Donde PARAMS son los parámetros necesarios que le especificamos a la librería para que pudiera hacer todo lo que le indicamos en el código.

Que por cierto, siendo honestos, esta librería comenzó como un client.py que estábamos copiando y pegando en los repositorios donde lo necesitábamos (hey! no, nos juzguen, ocupábamos movernos rápido). Su invocación se miraba algo similar a esto:

- name: Create project app on CF Pages
        env:
          ENVS NECESARIAS AQUI
        run: |
          echo "Installing requirements"
          python -m pip install --upgrade pip >/dev/null
          pip install -r requirements-deploy-apps.txt >/dev/null
          echo "Finished installing requirements"
          python client.py

Pero como estarás pensando esto implicaba un enorme problema de escalabilidad y mantenibilidad, por ello terminamos migrando este archivo de Python a una librería. Y ahora su invocación se ve algo así:

- run: |
  pip install {URL_PRIVADA_DONDE_VIVE_LA_LIB}

- name: Createproject app on CF Pages
  env:
    ENVS NECESARIAS AQUI
  run: |
    pyflaredeployer pages create-project

Para ello utilizamos:

  • Cloudflare API: para saber qué llamadas y con qué parámetros, ocupábamos hacer.
  • Python: para crear la librería.
    • En este punto quiero resaltar la importancia del trabajo colaborativo y tener muy presente que consultar al team es un gran, enorme, gigantesco perk. Queríamos una manera “fácil” y rápida de poder generar esta librería, así que consultamos con un experto de Python de nuestro team, el Sr. Eduardo Álvarez; en un Zoom, nos habló sobre Typer, y en un par de minutos ya teníamos nuestra librería diciendo ‘Hola mundo’.
  • GitHub Action: para controlar los eventos y flujos del CI/CD.
  • Mucha paciencia, prueba y error.

Conoce el producto final del equipo de infra

Untitled (4).png

Donde cada platzialgo.com es un proyecto totalmente autónomo e independiente, pero a la vez compatible con todos los servicios internos que ya utilizamos.

Al final pudimos construir nuestra librería, pudimos armar los Github Actions súper custom, optimizados para nuestros proyectos y además escalables porque utilizamos Reusables Actions, ya que con el código de los Actions pasaba algo muy similar, era el mismo, se copiaba y pegaba. Y en este caso pasamos de tener Actions de más de 160 líneas a unos Actions de 15 líneas, que se ven algo así:

name: ACTION DE DEPLOY

on:
  pull_request:
    types: [ labeled ]
  workflow_dispatch:

jobs:
  deploy-app:
    if: ${{ github.event.label.name == 'staging' }}
    uses: URL_DEL_ACTION_REUSABLE
    with:
      appName: 'NOMBRE_APP'pathApp: 'PATH_APP'secrets: inherit

Otro importante logro de todo esto, fue que al nosotros manejar todo el ciclo del CI/CD pudimos optimizar los tiempos de compilación de las aplicaciones. Cuando se lo delegábamos a CF Pages con su integración automática, se tardaba alrededor de 10 min. Con nuestros Actions ese tiempo se redujo un 50% aproximadamente.

Y con esto pudimos entregarle un bonito y poderoso DevExp a nuestros Frontends.

Qué aprendizajes nos llevamos de todo esto:

Hay 3 aprendizajes muy importantes:

  • Para todo problema existe una solución y para encontrarla a veces hay que escarbar el problema desde lo más profundo.
  • Aprende a combinar las tecnologías según el problema, habitualmente hay una nueva herramienta esperando a ser descubierta por ti.
  • Nunca pares de ser curioso, nunca pares de aprender.

Te recomiendo:

Cuéntame si has tenido retos similares y como los resolviste. ¡Fue un gusto que me acompañaras en este blog, hasta la próxima!

Fernando
Fernando
fernandomunoz

15872Puntos

hace un año

Todas sus entradas
Escribe tu comentario
+ 2
1
260Puntos
Buen día , y como construimos una biblioteca virtual de marketing para estudiantes de Instituto Estatal Trujillo ?