Javier Valencia Javier Valencia
Programador trabajando en su entorno de desarrollo

Rails sostenible (II): arranca tu app con buen pie

Javier Valencia · · 6 min de lectura · 2 visitas · Desarrollo
rails ruby rails-sostenible devops docker tooling

Segunda entrega de la serie Rails sostenible, basada en Sustainable Web Development with Ruby on Rails de David Bryant Copeland. Si te perdiste el arranque, empieza por la primera entrega. Tiempo de lectura estimado: 13 minutos.

En el post anterior vimos que la sostenibilidad es la capacidad de seguir cambiando el software a coste constante. Pues bien: el primer coste que se dispara en cualquier proyecto mal arrancado es el de poner a alguien a trabajar en él. Copeland le dedica un capítulo entero a algo que la mayoría de equipos descuida —el setup del entorno— porque es el primer sitio donde la insostenibilidad muerde.

La tesis del capítulo es simple: un desarrollador nuevo debería poder clonar el repositorio y tener la aplicación corriendo con un solo comando, sin un documento de veinte pasos en Notion que nadie ha actualizado desde 2023.

Crear la app pensando en producción

Copeland empieza por rails new, pero con criterio. Recomienda usar PostgreSQL desde el principio en lugar de SQLite, para que el entorno de desarrollo se parezca a producción (evitar el clásico "en mi máquina funciona" por diferencias de base de datos):

rails new mi_app --database=postgresql

El principio detrás es el de paridad dev/prod del manifiesto 12-factor: cuanto más se parezcan tus entornos, menos sorpresas en el despliegue. No tiene sentido desarrollar sobre SQLite y rezar para que las queries funcionen igual en el Postgres de producción.

Configuración por el entorno, no por el código

Todo lo que varía entre entornos —credenciales, URLs de servicios externos, claves de API— va en variables de entorno, nunca hardcodeado ni commiteado. Esto es de nuevo 12-factor, y Rails lo soporta de forma natural con ENV:

# config/initializers/stripe.rb
Stripe.api_key = ENV.fetch("STRIPE_API_KEY")

Fíjate en el fetch en lugar de ENV["..."]. Con fetch, si la variable no existe, la aplicación peta al arrancar con un error claro, en vez de fallar misteriosamente en runtime cuando ya es tarde. Es un patrón pequeño con un gran retorno: convierte un fallo silencioso en uno ruidoso y temprano.

dotenv para el desarrollo local

Escribir STRIPE_API_KEY=... rails server a mano cada vez es insostenible. Copeland recomienda la gema dotenv para cargar variables desde ficheros .env en desarrollo y test:

# .env.development (este SÍ se commitea: valores de ejemplo, no secretos reales)
STRIPE_API_KEY=sk_test_xxxxxxxx
DATABASE_URL=postgres://localhost/mi_app_development

La regla es: .env.development y .env.test con valores de ejemplo van al repositorio para documentar qué variables hacen falta; los secretos reales van en ficheros ignorados por git (.env*.local) o en el gestor de secretos del entorno de producción.

Resaltado de sintaxis de Ruby en el editor

bin/setup: un comando para gobernarlos a todos

Aquí está la joya del capítulo. Rails genera un bin/setup por defecto, pero Copeland lo reescribe para convertirlo en un script idempotente, ruidoso y a prueba de fallos. La idea: ejecutarlo en una máquina recién clonada deja la app lista; ejecutarlo de nuevo no rompe nada.

#!/usr/bin/env ruby
require "fileutils"

APP_ROOT = File.expand_path("..", __dir__)

def system!(*args)
  system(*args, exception: true)   # peta si el comando falla
end

FileUtils.chdir(APP_ROOT) do
  puts "== Instalando dependencias =="
  system!("bundle check") || system!("bundle install")

  puts "\n== Preparando la base de datos =="
  system!("bin/rails db:prepare")   # crea/migra solo si hace falta

  puts "\n== Limpiando logs y temporales =="
  system!("bin/rails log:clear tmp:clear")

  puts "\n== Listo. Arranca con: bin/run =="
end

Tres detalles que importan:

  • system! con exception: true hace que el script se detenga en el primer error en vez de seguir adelante dejando un entorno a medias.
  • bundle check || bundle install evita reinstalar gemas si ya están: idempotencia.
  • db:prepare crea la base de datos si no existe y la migra si hace falta, sin quejarse si ya estaba. Es la pieza que hace el script repetible.

Si tu README tiene una sección "Cómo montar el entorno" con más de dos líneas, esa sección es un bug. Debería poner: git clone ... && bin/setup.

bin/run para levantar todo a la vez

Una app real no es solo el servidor web: hay un proceso de assets (esbuild, tailwind), quizá un worker de jobs, quizá un proceso de CSS. Pedirle a alguien que abra cuatro terminales es frágil. Copeland usa un Procfile.dev y una herramienta tipo foreman u overmind para levantar todo con un comando:

# Procfile.dev
web: bin/rails server -p 3000
css: bin/rails tailwindcss:watch
worker: bundle exec sidekiq
# bin/run
#!/usr/bin/env bash
exec foreman start -f Procfile.dev "$@"

Rails 7 ya genera algo parecido con bin/dev; la idea de Copeland es la misma, anterior a que fuera la convención: un comando levanta el entorno completo.

bin/ci: la misma puerta de calidad en local y en CI

Esta es otra idea que me parece de oro. En vez de tener la configuración de los checks dispersa en un YAML de GitHub Actions que solo se ejecuta en la nube, Copeland mete toda la verificación de calidad en un script versionado, bin/ci, que ejecutas igual en tu máquina que en el servidor de integración:

#!/usr/bin/env bash
set -e   # aborta al primer fallo

echo "== Tests =="
bin/rails test test:system

echo "== Análisis de seguridad (Brakeman) =="
bundle exec brakeman --quiet --no-pager

echo "== Vulnerabilidades en dependencias =="
bundle exec bundler-audit check --update

echo "== Linter (RuboCop) =="
bundle exec rubocop

echo "== CI OK =="

El pipeline de CI se reduce entonces a una línea: bin/ci. Las ventajas de sostenibilidad son enormes:

  • No hay deriva entre lo que comprueba tu máquina y lo que comprueba el servidor.
  • Puedes reproducir un fallo de CI en local al instante, sin hacer push a ciegas.
  • La configuración vive en el repo, versionada, no atrapada en la UI de una herramienta concreta.

Copeland incluye en la puerta de calidad dos cosas que mucha gente olvida: Brakeman (análisis estático de seguridad para Rails) y bundler-audit (avisos de CVE en tus gemas). Seguridad y dependencias revisadas en cada commit, gratis.

Logging de producción con lograge

El logger por defecto de Rails escribe varias líneas por petición, en un formato pensado para leerse en una terminal de desarrollo. En producción eso es un desastre para cualquier sistema de observabilidad: imposible de parsear, imposible de agregar. Copeland recomienda lograge, que colapsa cada petición en una sola línea estructurada:

# config/environments/production.rb
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new
config.lograge.custom_options = lambda do |event|
  { request_id: event.payload[:request_id], host: event.payload[:host] }
end

De varias líneas ruidosas por request pasas a un JSON por línea con método, ruta, status, duración y los campos que tú añadas. Esto enlaza directamente con el capítulo de operaciones que veremos en la última entrega: sin buenos logs no hay observabilidad, y sin observabilidad mantener algo en producción es navegar a ciegas.

Docker, pero para los servicios

El libro dedica un apéndice a Docker, y la postura de Copeland es matizada y muy sensata. Docker es excelente para reproducir los servicios de apoyo —PostgreSQL, Redis— de forma idéntica en todas las máquinas. Un docker-compose.yml para levantar la base de datos es puro carrying cost negativo: ahorras a cada persona instalar y configurar Postgres a mano.

# docker-compose.yml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres
    ports: ["5432:5432"]
    volumes: ["pgdata:/var/lib/postgresql/data"]
  redis:
    image: redis:7
    ports: ["6379:6379"]
volumes:
  pgdata:

Ahora bien, dockerizar todo el ciclo de desarrollo —correr el propio Rails dentro de un contenedor, con el código montado por volumen— es otra historia. Tiene un carrying cost real: lentitud de I/O en algunos sistemas, complejidad de debugging, otra capa que entender. Copeland no lo prohíbe, pero te invita a sopesarlo: para muchos equipos, Ruby instalado en local + servicios en Docker es el punto óptimo.

Mi versión

El bin/ci me cambió la forma de trabajar. Antes vivía con la angustia del "a ver si pasa CI" después de cada push. Tener un único script que ejecuta lo mismo en local me devolvió el control: si bin/ci pasa en mi máquina, pasa en el servidor, punto. Lo replico incluso fuera de Rails —en mis proyectos Go tengo un bin/ci equivalente que corre go test, golangci-lint y govulncheck.

Sobre bin/setup: la prueba de fuego que hago es borrar todo y clonar el repo en una carpeta limpia. Si bin/setup no me deja la app corriendo, está roto, por mucho que "en mi máquina ya funcione". Es la mejor inversión anti-insostenibilidad que conozco, porque el coste de un onboarding malo se paga con cada persona nueva.

Y lo de ENV.fetch en vez de ENV[] parece una tontería hasta que te ahorra una tarde de debugging persiguiendo un nil que se propagó tres capas hacia abajo. Fallar pronto y ruidosamente es, casi siempre, lo sostenible.

Lo que viene

Con el entorno reproducible y la puerta de calidad montada, ya podemos escribir código sin miedo. En el próximo post entramos en el corazón de todo el libro y de esta serie: por qué la lógica de negocio no va en los Active Records, qué es el seam del que habla Copeland, y cómo darle a esa lógica una casa propia mediante servicios stateless con nombres explícitos.