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 🤗