# Migraciones Documentación:
Para las migraciones de aplicaciones Rails a entorno actual que tenemos en AWS donde usamos ECS para levantar las aplicaciones con Contenedores de Docker.
## Dockerización
Como primer paso hay que Dockerizar la aplicación, y lograr levantar la aplicación con Docker Compose replicando la arquitectura de donde utilizamos un nginx junto con la aplicación, a continuación agrego un `Dockerfile` y `docker-compose.yml` a modo de ejemplo.
##### Dockerfile
```dockerfile
FROM ruby:2.3.8
MAINTAINER Snappler
# Install apt based dependencies required to run Rails as
# well as RubyGems. As the Ruby image itself is based on a
# Debian image, we use apt-get to install those.
RUN apt-get update -qq && apt-get install -y build-essential nodejs
# Hora de Argentina en el server
RUN cp /usr/share/zoneinfo/America/Buenos_Aires /etc/localtime
# Configure the main working directory. This is the base
# directory used in any further RUN, COPY, and ENTRYPOINT
# commands.
RUN mkdir -p /app
WORKDIR /app
# Copy the Gemfile as well as the Gemfile.lock and install
# the RubyGems. This is a separate step so the dependencies
# will be cached unless changes to one of those two files
# are made.
COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install --jobs 20 --retry 5
# Copy the main application.
COPY . ./
# assets_precompile
RUN RAILS_ENV=production bundle exec rake assets:precompile
# Expose port 80 to the Docker host, so we can access it
# from the outside. Could be override.
EXPOSE 3000
ENTRYPOINT ["/app/docker/docker-entrypoint.sh"]
CMD bundle exec unicorn -c docker/unicorn.rb --no-default-middleware
```
> [color=lightblue] **Notas sobre este `Dockerfile`:**
> - Utiliza como base una imagen de `ruby:2.3.8`, cambiarlo para la version que utiliza la aplicación (como recomendación actualizaría la version de Ruby, como mínimo a la más actual de la version que usa, ejemplo, si usa `2.3.x` cambiarlo a `2.3.8`, si usa `2.4.x` subir a `2.4.9`).
> - Contempla una applicacion rails completa, en el caso de tratarse de una API, no haría la dependencia de `nodejs` que se instala en el primer comando `RUN`, ni hacer la precompilacion de los assets.
> - Utiliza `unicorn` como Ruby Webservers, esto se ve reflejado en el `CMD` de este Dockerfile.
> - Define un `ENTRYPOINT`, que se ejecutará siempre que el Docker sea levantado, útil para tirar comandos como `rails db:migrate` de forma automática ya que las aplicaciones siempre necesita las migraciones actualizadas.
##### docker-compose.yml
```yaml
version: '3'
services:
nginx:
image: snappler/nginx-reverse-proxy:latest
environment:
- REQUEST_TIMEOUT=60
- DOCKER_NAME=app
- DOCKER_PORT=3000
ports:
- 80:80
depends_on:
- app
links:
- app
app:
build: .
environment:
- DATABASE_HOST=db
- DATABASE_NAME=database_name
- DATABASE_USER=root
- DATABASE_PASS=root
- RAILS_ENV=production
- RAILS_LOG_TO_STDOUT=true
ports:
- 3000:3000
depends_on:
- db
links:
- db
job:
build: .
command: 'bundle exec rake jobs:work'
environment:
- DATABASE_HOST=db
- DATABASE_NAME=database_name
- DATABASE_USER=root
- DATABASE_PASS=root
- RAILS_ENV=production
- RAILS_LOG_TO_STDOUT=true
depends_on:
- db
links:
- db
db:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=root
ports:
- 3306:3306
```
> [color=lightblue] **Notas sobre este `docker-compose.yml`:**
> - Para preparar la aplicación hay que parametrizar la misma con variables de entornos, ejemplo en projectos viejos la configuracion de la base de datos de Rails (database.yml) estaba ignorada y se configuraba directamente en el servidor. Pero en este nuevo formato de trabajo se deja de ignorar el archivo y se lo deja dinámico a partir de variables de entornos, abajo dejo un ejemplo
> - Se contempla que la aplicación cuenta con jobs en background (en el ejemplo se usa `rake jobs:work` de la gema [`delayed_job`](https://github.com/collectiveidea/delayed_job)). En caso de no usar borrar el servicio
> - Se usa una imagen de nginx de snappler para que funcione de proxy reverso. Permite manejar la alta demanda en las aplicaciones.
##### database.yml
```yaml
default: &default
adapter: mysql2
pool: <%= ENV['DATABASE_POOL'] || 5 %>
timeout: <%= ENV['DATABASE_TIMEOUT'] || 5000 %>
host: <%= ENV['DATABASE_HOST'] %>
database: <%= ENV['DATABASE_NAME'] %>
username: <%= ENV['DATABASE_USER'] %>
password: <%= ENV['DATABASE_PASS'] %>
<%= Rails.env %>:
<<: *default
```
> [color=orange] **Importante!**
En nuestra implementación del Dockerfile estamos contemplando con dos archivos en la applicacion, `docker/docker-entrypoint.sh` el cual se usa en el comando `ENTRYPOINT` y `docker/unicorn.rb` con la configuracion de unicorn con la cual se levanta la Aplicación.
##### docker/docker-entrypoint.sh
```bash
#!/bin/bash
# https://stackoverflow.com/a/38732187/1935918
set -e
if [ -f /app/tmp/pids/server.pid ]; then
rm /app/tmp/pids/server.pid
fi
if [ -f /app/tmp/unicorn.pid ]; then
rm /app/tmp/unicorn.pid
fi
if [[ -z $REQUEST_TIMEOUT ]]; then
REQUEST_TIMEOUT=60
fi
if [[ $MIGRATE = "true" ]]; then
bundle exec rake db:migrate
fi
sed -i "s/REQUEST_TIMEOUT/${REQUEST_TIMEOUT}/" /app/docker/unicorn.rb
exec "$@"
```
##### docker/unicorn.rb
```ruby
app_dir = "/app"
working_directory app_dir
pid "#{app_dir}/tmp/unicorn.pid"
worker_processes 1
listen 3000
timeout 60
```
## Cambios que require la Aplicación
Para migrar una aplicación a ECS de AWS hay consideraciones a tener en cuenta con el fin de que la misma se ejecute correctamente en este nuevo entorno:
1. ¿Tiene Uploads? Usar S3.
2. ¿Tiene tareas periodicas? Usar [Elastic Whenever](https://github.com/wata727/elastic_whenever).
3. ¿Usa la cache de Rails? Configurar para que use Redis como medio de cache.
4. ¿Usa Cookies? Usar RailsCache para almacenarla.
5. ¿Envia mail? Enviarlos en background [`sidekiq`](https://github.com/mperham/sidekiq) o [`delayed_job`](https://github.com/collectiveidea/delayed_job).
6. Precompilacion de Assets
En cada apartado dejo links a las documentaciones oficiales de cada gema, donde estan mejor explicado su configuración de lo que puedo explicar en este documento. En lo que voy a centrarme es en explicar problemas comunes que se pueden encontrar y consideraciones que tienen que tener en cuenta pensando en que estas aplicaciones se van a encontrar en un entorno dockerizado en ECS.
### Uploads con S3
Si la aplicación cuenta con Uploads, lo mas probable es que los este trabajando con alguna de las dos soluciones más populares que hay para Rails:
- [ActiveStorage](https://edgeguides.rubyonrails.org/active_storage_overview.html)
- [Carrierware](https://github.com/carrierwaveuploader/carrierwave)
Con ActiveStorage solo habra que agregar la dependencia del SDK de Amazon para S3 [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby).
Con Carrierware hay agregar un adapter que permita integrar el mismo con AWS, las que conosco son:
- [fog-aws](https://github.com/fog/fog-aws)
- [carrierware-aws](https://github.com/sorentwo/carrierwave-aws)
La que mejor me ha funcionado es `fog-aws`
> [color=orange] **Importante:**
No agregar como dependencia el SDK completo de AWS, ya que contiene muchas gemas incluidas dentro del mismo. Solo agregar las dependencias necesarias.
### Elastic Whenever
La gema más popular para ejecutar tareas periodicas, si no es la única, es [`whenever`](https://github.com/javan/whenever):
Si el projecto cuenta con esta gema, se veria reflejado en el Gemfile, pero para asegurase que se esta usando, porque en algunos projectos estan agregados pero jamas se utilizaron, hay que comprobar si tiene un archivo `config/schedule.rb` con la definición de las tareas.
Whenever y ElasticWhenever compatibles, por lo cual solo requiere hacer el cambio de la gema. Un detalle importante es que ElasticWhenever registra las tareas a correr en el Schedule de un Cluster AWS y las registra con horario `UTC`. Por ejemplo si hay una tarea que se ejecuta diariamente a las `8:00 am` en la Argentina, como nuestro uso horario es `UTC-3`, hay que registrar la tarea para que se ejecute a las `5:00` am para compensar la diferencia horaria.
### Rails Cache
Si la aplicacion esta haciendo uso de la cache de Rails, hay que asegurarse que este configurado con **Redis**, esto se debe a que al trabajar con Docker en ECS, tenemos que tener en cuenta que existe la posibilidad de levantar varios contenedores con la misma aplicación, y si no usamos un servicios externo a la aplicación puede que cada contenedor tenga en su cache local información diferente.
> [color=lightblue] **Ejemplo**
Es posible que se este usando para evitar consultas pesadas y recurrentes a la base de datos, pero si la información guardada cambia se deberia invalidar la cache.
Esto podría ocacionar inconsistencia en las respuestas si la cache no esta centralizada en un servicio como **Redis**, ya que si cada contenedor usa cache en memoria, al invalidar una cache produciria que solo se invalide en el contenedor que la invalido, y los otros contenedores no se verían afectado
Cuando hablamos de la cache de Rails la misma cuenta con diferentes adapter para implementar la cache:
- Redis
- MemCache
- Memory
- File
Pero como hemos dicho nosotros normalmente usamos **Redis**, y dependiendo de la version de Rails requiere diferente configuraciones:
##### Rails >= 5.2
A partir de la version **5.2** Rails provee un adapter para manejar nuestra cache con Redis, para mas información puede ver la [Guía de Rails](https://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-rediscachestore)
##### Rails < 5.2
Para las versiones previas a la **5.2** su configuranción es muy similar pero requiere agregar al projecto la gema `redis-rails` que implementa el adapter para Rails, vea su [GitHub](https://github.com/redis-store/redis-rails) para ver la configuración exacta.
En su configuracion usar variables de entornos para la configuracion de Redis, ejemplo:
```yaml
REDIS_HOST: localhost
REDIS_PORT: 6379
REDIS_DATABASE: 1
REDIS_NAMESPACE: application_name
```
Aunque utilizar **namespace** no sea necesario, sería lo mejor de ser posible, para poder reutilizar un mismo servicio de Redis para diferentes aplicaciones y minimizar recursos a levantar en AWS.
### Cookies y Session
Actualmente no nos encontramos con ningun problema con la session de un usuario conectado, porque esto lo maneja desde el lado del cliente, usando cookies encriptadas para las sessions. Ver ventajas y limítaciones [aquí](https://guides.rubyonrails.org/security.html#session-storage).
Pero en caso que las sessions esten configurada de tal forma que información de la session se encuentre del lado del Servidor es probable que haya problemas de cosistencia entre los contenedores.
Para estos casos Rails provee la posibilidad de utilizar Redis de la misma forma que lo utiliza para la cache.
Dejo un post de Medium [aquí](https://medium.com/@kirill_shevch/configuration-cache-and-rails-session-store-with-redis-b3ce6f64d1fc), que muestra con configurarlo.
### Jobs in Background
Para el envio de mail y para realizar operaciones pesadas, y para toda operación que se puede realizar de forma asincrona, Rails provee la ActiveJob con integracion a varias herramientas:
- [sidekiq](https://github.com/mperham/sidekiq/wiki/Active-Job)
- [rescue](https://github.com/resque/resque/wiki/ActiveJob)
- [delayed_job](https://github.com/collectiveidea/delayed_job#active-job)
- [sneakers](https://github.com/jondot/sneakers/wiki/How-To:-Rails-Background-Jobs-with-ActiveJob)
Cada adapter tiene su particularidades, especialmente desde el punto de vista de arquitectura, ya que mientras **`sidekiq`** y **`rescue`** usan **Redis** para implementar las colas de trabajos, **`delayed_job`** utiliza la base de datos (**MySQL**, **PostgreSQL** o **MongoDB**) y **`sneakers`** lo implementa con **RabbitMQ**
Nosotros normalmente utilizamos **`sidekiq`** o **`delayed_job`**, el principal criterio para elegir uno sobre el otro, por lo menos de mi parte, es si la aplicacion ya cuenta con **Redis** elijo **`sidekiq`**, pero en el caso de no contar con **Redis** prefiero utilizar **`delayed_job`** con un adapter con la misma base de datos con el cual ya cuenta la aplicación, para evitar una dependencia extra de recursos.
### Precompilacion de Assets
Cada versión de Rails precompila los assets de forma distinta:
...continuará