Optimizando costos en GitHub Actions
Recientemente en EasyBroker migramos nuestro sistema de Integración continua a GitHub Actions.
Inicialmente configuramos un Workflow con 12 contenedores considerando que era una buena idea para identificar rápidamente donde fallaron las pruebas.
En promedio cada contenedor tardaba 6 minutos en completar su tarea, mientras que 2 contenedores en específico (modelos y un grupo de controladores) tardaban en promedio 12 minutos.
Después de una semana trabajando con esta nueva implementación recibimos un mensaje de GitHub alertándonos que estábamos por llegar al límite de uso de la capa gratuita que GitHub nos provee 💸💸💸, por lo que decidimos investigar.
En primer lugar notamos que de los 6 minutos que tardaban los 10 contenedores más rápidos, en promedio 4 minutos eran tareas de configuración.
Posteriormente notamos que a pesar de tener el cache “configurado”, nuestro contenedor estaba descargando las gemas una y otra vez.
Y finalmente solo se invertían entre 20 y 50 segundos en ejecutar las pruebas.
Ahorrándonos unos minutos
Lo primero que decidimos hacer fue pasar de 12 contenedores a 2 contenedores. Considerando que las pruebas de los controladores de agentes son las que toman más tiempo, decidimos que lo modelos podían convivir con las pruebas de los 10 contenedores que tardaban 6 minutos en promedio, terminando con una configuración como la siguiente.
Antes
strategy:
fail-fast: false
matrix:
test_folder: [models, helpers, jobs, lib, mailers, presenters, services, controllers/*.rb, controllers/admin, controllers/agent, controllers/webhooks, controllers/api]
Ahora
strategy:
fail-fast: false
matrix:
test_folder: [models helpers test/jobs test/lib test/mailers test/presenters test/services test/controllers/*.rb test/controllers/admin test/controllers/webhooks testcontrollers/api, controllers/agent]
Lo siguiente fue actualizar el apartado de cache para hacerlo funcionar, nuestra configuración utilizaba la versión 1 de actions/cache e incluía la opción de restore-keys la cual según la documentación es opcional.
- name: Gem cache
uses: actions/cache@v1
with:
path: vendor/bundle
key: $-gems-$
restore-keys: |
$-gems-
Por lo que simplemente actualicé a la versión 2 de la acción y removí la opción de restore-keys al considerar que no es algo que requerimos aún.
- name: Gem cache
uses: actions/cache@v2
with:
path: vendor/bundle
key: $-gem-use-ruby-$
Además de eso, revisando el paso donde se instalan las dependencias, encontré que lo teníamos fusionado con el paso para crear la base de datos y nos mostraba un deprecation warning al momento de ejecutar bundle install con la bandera para indicar la ruta de instalación de las gemas.
- name: Bundle Install and Create DB
env:
RAILS_ENV: test
DB_PASSWORD: root
DB_PORT: $
run: |
sudo /etc/init.d/mysql start
cp config/database.ci.yml config/database.yml
gem install bundler --version 2.0.2 --no-ri --no-rdoc
bundle install --jobs 4 --retry 3 --path vendor/bundle
bin/rails db:setup
[DEPRECATED] The `--path` flag is deprecated because it relies on being remembered across bundler invocations, which bundler will no longer do in future versions. Instead please use `bundle config set path 'vendor/bundle'`, and stop using this flag
Por lo que opté por separarlo en 2 pasos independientes y actualizar la forma en la que bundler detecta la ubicación de las gemas.
- name: Bundle install
run: |
gem install bundler --version 2.0.2 --no-ri --no-rdoc
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Create DB
env:
RAILS_ENV: test
DB_PASSWORD: root
DB_PORT: $
run: |
sudo /etc/init.d/mysql start
cp config/database.ci.yml config/database.yml
bin/rails db:setup
Con estos pequeños cambios logramos pasar de 4 minutos en la instalación de las gemas a tan solo 4 segundos.
Antes
Ahora
Como paso final para esta primer etapa de optimización, revisé los pasos dónde realizábamos la instalación de algunas dependencias por apt-get y encontré que algunos eran innecesarios. Por ejemplo, cuando verificábamos la conexión a MySQL, estábamos tratando de instalar el cliente que ya está disponible y esto tomaba 20 segundos en promedio (entre verificar la instalación y verificar que la conexión fue exitosa) con solo remover la llamada de apt-get que no era necesario logramos bajar a un promedio de 6 segundos.
Antes
Ahora
Notas finales
Esta fue la primer etapa de nuestra actualización para tener un sistema de integración continua saludable y eficiente, logramos pasar de un promedio de 1 hora 30 minutos por ejecución.
A tan solo 20 minutos en promedio.
Como experiencia nos llevamos que, en servicios donde la factura es por tiempo de uso, el ahorrar segundos es crucial y es importante poner atención a los detalles por pequeños que sean. Como en el caso de la instalación de dependencias que ya existen en el contenedor. Puede que 13 segundos ahorrados suene a nada, pero si lo multiplicamos por el número de builds que tiene tu empresa al día, puede que logres ahorrar un par de horas en tu factura.
Lo siguiente que haremos será tratar de mover la instalación de dependencias que requieren ser instaladas manualmente a un contenedor Docker (aún necesito investigar si es posible esto) y utilizar alguna herramienta como TestProf para identificar los tests más lentos y ver la manera de optimizarlos.