Mantenimiento: 19 de mayo, a las 14:00 UTC, est: 15 minutos

Review apps en Heroku: Scripts personalizados – parte 3

Por

Unsplash

En la segunda parte de esta serie mostramos cómo configuramos una review app en Get on Board.

# Ejecuta tareas tipo rake al crear y destruir la review app
“scripts”: {
 “postdeploy”: “rails getonbrd:heroku:review_app_setup getonbrd:db:create_sample_data”,
 “pr-predestroy”: “rails getonbrd:heroku:review_app_predestroy”
}

Lo que queda es revisar en detalle cómo es posible personalizar una aplicación con código que se ejecuta una vez que esta es creada — postdeploy — o después de ser destruida — pr-predestroy.

Data seeds

Es conveniente alimentar una review app con un set mínimo de datos de pruebas. Rails provee db:seeds, sin embargo, cuando tienes mucha data organizada en múltiples modelos dependientes entre sí, se hace incómodo tenerlo todo en un mismo archivo.

# companies.yml
DEFAULTS: &DEFAULTS
  name: $LABEL
  email: dev+company_$LABEL@getonbrd.com
  web: https://www.$LABEL.com
getonbrd:
  <<: *DEFAULTS
  name: Get on Board
  description: Awesome jobs for awesome people.
  country: CL
...

A nosotros se nos ocurrió hacer uso de otro mecanismo del framework, que usualmente se usa para alimentar el ambiente de testing con datos de muestra. Entre las ventajas de usar fixtures está la de declarar la data en archivos YAML ☝️, formato amigable que resulta conveniente cuando — como es nuestro caso — colaboradores que no necesariamente entienden código pueden agregar nuevos registros.

Para cargar la muestra en la review app creamos una tarea que importa los fixtures y ejecuta un par de métodos de clean-up y sanity check de la data:

# rake file
task create_sample_data: :environment do
 CreateSampleData.run(false)
end
# CreateSampleData.rb
class CreateSampleData
  def self.run(perform_reset_db =false)
    new.run(perform_reset_db)
  end
  def run(perform_reset_db = false)
    reset_db if perform_reset_db
    without_contraints { load_fixtures }
    fill_long_texts
    update_tenant_hostnames
    ...
    sanity_check
  end
  
  ...
  
  def without_contraints
    drop_constraints
    yield
  ensure
    create_constraints
  end
  def load_fixtures
    perform 'Loading fixtures' do
      Rake::Task['db:fixtures:load'].invoke
    end
  end
  
  ...
end
Protip: No es posible cargar fixtures en una base de datos diferente a testing porque se violan constraints tales como primaries o secondaries keys. Por eso ejecutamos la carga dentro de un bloque que, usando funcionalidades nativas de PostgreSQL, eliminan los constraints — drop_constraints — por un rato, recreándolos — create_constraints— una vez que la data es cargada.

Dominios personalizados

Heroku provee un dominio en la forma [nombre-de-la-app].herokuapp.com cuando una aplicación es creada y no todas necesitan cambiarlo para funcionar. Get on Board, por su naturaleza multi-tenant necesita configurar dominios personalizados para los países donde está presente. La forma de hacer esto es crear registros CNAME en un DNS externo que apunten a la review app. Heroku documenta bien el proceso manual.

DNSimple es un proveedor de dominios que provee una API — y una gema ruby — con la que puedes interactuar para modificar las tablas DNS de tus dominios por cinco dólares al mes.

Platform API — y su gema en ruby — es la forma de interactuar con Heroku desde el código de tu aplicación.

Uniendo a ambos es posible crear dominios personalizados una vez que la aplicación ya existe:

task :review_app_setup do
 |   GOB_DEV_DOMAIN = #<our-custom-domain-for-dev>
 |   require "dnsimple"
 |   require "platform-api"
 | 

 |   heroku_app_name = ENV["HEROKU_APP_NAME"]
 |   dnsimple_account_id = ENV["DNSIMPLE_DEV_ACCOUNT_ID"]
 | 

 |   type = { type: 'CNAME' }
 | 

 |   dnsimple_client = Dnsimple::Client.new(access_token: ENV["DNSIMPLE_ACCESS_TOKEN"])
 |   heroku_client = PlatformAPI.connect_oauth(ENV["HEROKU_API_TOKEN"])
 | 

 |   # set DEFAULT_HOST env var
 |   heroku_client.config_var.update(
 |     heroku_app_name,
 |     { DEFAULT_HOST: "#{heroku_app_name}.#{GOB_DEV_DOMAIN}"}
 |   )
 |   # enable ACM (https)
 |   heroku_client.app.enable_acm(heroku_app_name)
 |   # Configure the custom domains in Heroku making sure the list
 |   # correspond with the tenants at `test/fixtures/tenants.yml`
 |   %w(home cl pe mx ar co re).each do |tenant|
 |     subdomain = if tenant === "home"
 |       "#{heroku_app_name}"
 |     else
 |       "#{heroku_app_name}-#{tenant}"
 |     end
 |     hostname = [subdomain, GOB_DEV_DOMAIN].join('.')
 | 

 |     # create the custom domain in Heroku
 |     heroku_client.domain.create(heroku_app_name, hostname: hostname)
 |     heroku_domain = heroku_client.domain.info(heroku_app_name, hostname)["cname"]
 | 

 |     # Create the CNAME record in DNSimple
 |     opts = type.merge({ name: subdomain, content: heroku_domain })
 |     # Query DNSimple to check whether the record already exist.
 |     resp = dnsimple_client.zones.zone_records(
 |       dnsimple_account_id,
 |       GOB_DEV_DOMAIN,
 |       { filter: { name_like: subdomain } }
 |     )
 | 

 |     # only create it if not found
 |     dnsimple_client.zones.create_zone_record(
 |       dnsimple_account_id,
 |       GOB_DEV_DOMAIN,
 |       opts
 |     ) if resp.data.empty?
 |   end
 | end

(Script para crear custom domains cuando se crea la review app)


Finalmente, una vez que se aprueban los cambios introducidos en la review app y el nuevo code se va a staging o producción, la app es destruida. En este punto podemos ejecutar un script para que haga housekeeping, como borrar las entradas CNAME creadas anteriormente en DNSimple:

task :review_app_predestroy do
  require “dnsimple”
  heroku_app_name = ENV[“HEROKU_APP_NAME”]
  
  # Wipe the DNS records
  dnsimple_account_id = ENV[“DNSIMPLE_DEV_ACCOUNT_ID”]
  
  dnsimple_client = Dnsimple::Client.new(
    access_token: ENV[‘DNSIMPLE_ACCESS_TOKEN’]
  )
  resp = dnsimple_client.zones.zone_records(
    dnsimple_account_id,
    GOB_DEV_DOMAIN,
    { filter: { name_like: “#{heroku_app_name}” } }
  )
  resp.data.each do |zone|
    dnsimple_client.zones.delete_zone_record(
      dnsimple_account_id,
      GOB_DEV_DOMAIN,
      zone.id
    )
  end
end


Como siempre, si te surgieron dudas o quieres compartir tips relacionados con este post puedes dejarnos un comentario en Discord o escribirnos directamente a team@getonbrd.com 🤗


Lo más reciente en Blog