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.
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:
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:
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.
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:
Sí… lo sé…
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.
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.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.
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í:
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í.
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:
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.
Hay 3 aprendizajes muy importantes:
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!