Imagen Docker para operaciones con EC2

He creado una primera versión de imagen en Docker para operaciones en Amazon Web Services. La escribí en Ruby e inicio con ella una serie de imágenes Docker para operaciones. Por ahora, tan solo hace las siguientes operaciones. Si pedimos ayuda al propio script:

Usage: ec2.rb [options]

Available options:
    -r, --region REGION              Supply a region
    -c, --create RELATIVE_PATH       Create the file(s) supplied
        --start INSTANCE[s]-ID       Start the INSTANCE[s]-ID supplied
        --stop INSTANCE[s]-ID        Stop the INSTANCE[s]-ID supplied
    -t, --terminate INSTANCE-ID      Terminate the INSTANCE-ID supplied
    -d, --describe                   Describe EC2 instances in region

    -v, --version                    Show version
    -h, --help                       Show this message

Pueden crearse instancias desde plantillas YAML almacenadas en un directorio. La versión es muy pre-alfa y seguramente irá evolucionando el modo de distribuir las plantillas con los dispositivos. Por ahora, consume cualquier archivo .yml en el directorio suministrado al script como parámetro.

La plantilla de ejemplo que incluyo en la imagen:

---
ec2:
  - instance:
    - params:
      - :image_id: 'ami-8fd760f6'
      - :min_count: '1'
      - :max_count: '1'
      - :key_name: 'name'
      - :security_group_ids: 'sg-00000000'
      - :user_data: ''
      - :instance_type: 't2.micro'
      - :placement:
        - :availability_zone: 'eu-west-1a'
      - :subnet_id: 'subnet-00000000'
      - :tag_specifications:
          - :tags:
            - :key: "Name"
            - :value: "Nom1"
  - instance:
    - params:
      - :image_id: 'ami-8fd760f6'
      - :min_count: '1'
      - :max_count: '1'
      - :key_name: 'name'
      - :security_group_ids: 'sg-00000000'
      - :user_data: ''
      - :instance_type: 't2.micro'
      - :placement:
        - :availability_zone: 'eu-west-1a'
      - :subnet_id: 'subnet-00000000'
      - tag_specifications:
          - :tags:
            - :key: "Name"
            - :value: "Nom2"

Referencia: EC2 Create Instances Method

Las credenciales las toma del directorio ~./aws y hay que montar la carpeta en el contenedor (cualquiera con una sugerencia para hacer la compartición de claves más segura será bienvenido/a).

La imagen Docker está aquí.

Asumiendo que tenemos en el directorio instances al menos una plantilla YAML con datos válidos en el mundo real, desde el contenedor Docker podemos crear las instancias de un modo similar a este:

docker run --rm -ti -v ~/.aws:/root/.aws -v ~/ruby-aws/instances:/ec2/instances gustavomrfz/ec2ops-ruby:0.5.1 ruby ec2.rb --create instances

Otras opciones, más obvias, son una descripción de las instancias en la zona de las credenciales, iniciar, apagar varias instancias y terminar una sóla instancia.

Un libro de Docker

Quiza os resulte útil definir un alias como

alias ec2op="docker run --rm -ti -v ~/.aws:/root/.aws -v ~/ruby-aws/instances:/ec2/instances gustavomrfz/ec2ops-ruby:0.5.1 ruby ec2.rb "

y de este modo simplificar el uso del script en la shell:

ec2op --start id-xxxxxxxxxxxx

Github: //github.com/gustavomrfz/ruby-aws

Docker Hub: //hub.docker.com/r/gustavomrfz/ec2ops-ruby/

Despliega manifiestos Puppet en remoto

Por sábado, 30 diciembre, 2017 0 , Permalink

El objetivo de los scripts en Bash del repositorio que enlaza este artículo es actualizar de manera remota un servidor Puppet 4 con los últimos cambios realizados en la rama master de un repositorio Git. Los scripts se distribuyen en un módulo de Puppet. Las variables de los scripts deberán ser modificadas por el operador para adaptarlas a las necesidades de su entorno.

Continuar Leyendo…

Puppet y relaciones de dependencia entre recursos

Por miércoles, 1 marzo, 2017 0 Permalink

Puppet declara dependencias de manera implícita, pero no siempre se cargan los recursos de la manera esperada. En Puppet hay tres maneras de declaraciones implícitas

  • title hash: Es una ordenación aleatoria pero consistente
  • manifest: Ordena siguiendo el orden de declaración en el manifiesto (por defecto en Puppet 4)
  • random: Aleatorio en cada ejecución. Es útil para debuggar problemas con relaciones de dependencia

Los metaparámetros de dependencia explicita proveen un medio de organización de dependencias muy potente. Veámoslo.

Ordenando recursos

En la siguiente declaración se declara una dependencia:

package { 'nginx':
       ensure => 'present',
       before => Service['nginx']

}

service { 'nginx':
       ensure => 'running',
       enabled => 'true',
       require => Package['nginx']
}

En este caso, el servicio ‘nginx’ se ejecutará sólo si el packete ‘nginx’ está presente. Los metaparámetros before y require son reduntanes en este caso y no ofrecen datos a otros recursos. En la declaraciones explicitas no es necesario declarar relaciones innecesarias, ya que hace más frágil el manifiesto.

Disparadores de eventos

Los metaparámetros notify y subscribe funcionan de manera similar a los dos anteriores, pero también enviarán eventos de refresco (refresh events) al recurso dependiente si la dependencia cambia. Por ejemplo, un servicio dependiente se recargará si su configuración cambia.

package { 'nginx':
       ensure => 'latest',
       notify => Service['nginx']

}

service { 'nginx':
       ensure => 'running',
       enabled => 'true',
       subscribe => Package['nginx']
}

Cuando el paquete nginx se actualice a la última versión, el recurso enviará un refresh event al servicio nginx para que se recargue. Ambos metaparámetros son redundantes.

El evento de refresco tiene un significado especial para recursos exec con el atributo refreshonly. Estos recursos sólo se aplicarán sí, y sólo si, la dependencia cambia.

file {'nombre':
  ensure  => 'file',
  source  => 'puppet://puppetserver/modules/module_name/name.sh',
  path => '/usr/local/bin/name.sh',
  notify => Exec['name.sh']
}

exec {'name.sh'':
  path => '/usr/local/bin',
  logoutput => 'true'
  refreshonly => true,
}

En script name.sh no se ejecutará a menos que el archivo se haya creado.

Flechas para encadenar recursos

La relaciones dependencia anteriores pueden declararse usando flechas aunque, sin embargo, su uso está desaconsejado por ser de lectura más difícil. Por ejemplo, para instalar Nginx antes que iniciar el servicio:

Package ['nginx'] -> Service['nginx']

Para envíar un evento de refresco:

Package ['nginx'] ~> Service['nginx']

Es posible expresarlo a la inversa, pero esto no habría que hacerlo nunca:

 Service['nginx'] <~ Package ['nginx']

Colectores

Un colector es un grupo de varios recursos. Hay que usarlo con precaución, ya que puede tener consecuencias inintencionadas nefastas. Se declara con el tipo de recurso seguido de <| atributo |> Por ejemplo:

User <||>                                          # Todos los usuarios declarados 
Service <| ensure == running |>    # Todos los servicios que están ejecutándose
Package <| tag == 'http' |>         # Todos los paquetes etiquetado con 'http'

La condición de búsqueda puede ser booleana:

Service <| (ensure == running) or (enabled == true ) |>
Service <| (ensure == running) and (title != system' ) |>

En resumen

Hemos visto los siguientes controles:

  • before y la flecha ->    Expresa una dependencia
  • notify y la flecha ~>    Envía un refresh event
  • require                           Exprea un recurso dependiente
  • subscribe                       Se aplica cuando recibe un refresh event
  • refreshonly                   Se ejectuta sólo cuando recibe un refresh event

Facter en Puppet

Por domingo, 19 febrero, 2017 0 Permalink

En mi último post, vimos como instalar Puppet en la arquitectura master-agent y la creación de un módulo para declarar algunos recursos. En este, quiero mostrar cómo configurar un parámetro utilizando la aplicación Facter y despues, cómo extender Facter para tomar ese parámetro desde un archivo que lo contenga, creado «a mano» en el agente. Utilizaré la configuración del post anterior de la serie automatizaciones para el ejemplo un servicio PHP-FPM

Modulo «zona horaria»

El modulo que crearemos se llamará zonahoraría. Tomará la zona horaria desde un parámetro del sistema y en la segunda parte del post modificaré el manifiesto para lea desde un fact a medida.

Instalando PHP

Instalamos el módulo de PHP:

cd /etc/puppetlabs/code/environment/test/modules
/opt/puppetlabs/bin/puppet module install mayflower-php --version 4.0.0-beta1

 Creando módulo «zonahoraria»

En el directorio de módulos:

/opt/puppetlabs/bin/puppet module generate enredadera-zonahoraria

 Creando init.pp

El init.pp del módulo utilizará la clase del módulo recién instalado (podéis encontar más información acerca del módulo en este enlace).

class zonahoraria::setup {

}

 Configuración global

class { '::php::globals':
    php_version			=> '7.0',
    config_root			=> '/etc/php/7.0',
}

 Configuración de PHP

class { '::php':
      ensure               => latest,
      manage_repos         => true,
      fpm                  => true,
      dev                  => true,
      composer             => true,
      pear                 => true,
      phpunit              => false,
      settings             =>	{
            'PHP/max_execution_time'  => '90',
            'PHP/max_input_time'      => '300',
            'PHP/memory_limit'        => '64M',
            'PHP/post_max_size'       => '32M',
            'PHP/upload_max_filesize' => '32M',
            'Date/date.timezone'      => $timezone,
            },
      extensions    => {
                          imagick   => {
                                           provider => pecl,
                         }
      }
}

La variable $timezone tiene el valor (en el agente):

/opt/puppetlabs/bin/facter timezone

CET

Precendecia

La clase ::php::global debe ejecutarse antes de la clase ::php, por lo que ha de utilizarse algún parámetro como require para garantizar el orden de ejecución o un operador de precedencia.

En el post anterior utilizamos el operador -> de un modo que en esta clase podríamos usar:

Class[::php::global] -> Class['::php]

No obstante, puede definirse la precedencia en las propias declaraciones de los recursos y, de este modo, la clase «zonahoraria» mostraría la siguiente estructura:

class zonahoraria::setup {

  class { '::php::globals':
            
            ...

 	}->
  class { '::php':
            
            ...

        }
}

Ejecutando el manifiesto

Pedimos el manifiesto desde el agente para su ejecución:

/opt/puppetlabs/bin/puppet agent -t

 Comprobación

Comprobamos que parámetro timezone.

cat /etc/php/7.0/fpm/php.ini | grep timezone

; Defines the default timezone used by the date functions
; http://php.net/date.timezone
;date.timezone =
date.timezone =CET

 Leyendo el parámetro desde un archivo en el agente

Para ello hay que escribir un archivo a mano en la máquina del agente y ampliar las funcionalidades de Facter para que lea este archivo.

Archivo con parámetro

Creamos el archivo con la zona horaria:

echo " Europe/Madrid" > /usr/local/etc/timezone

 Montaje del directorio

Editamos de nuevo el archivo fileserver.conf  y añadimos el punto de montaje /usr/local/etc:

[files]
  path /usr/local/bin:/usr/local/etc
  allow *

 Ampliando el comportamiento facter

Facter permite añadir nuevos «hechos», que pueden escribirse en Ruby, y utilizarse en los manifiestos.

Creamos un fact en el módulo «zonahoraria»:

mkdir -p /etc/puppetlabs/code/environments/test/modules/zonahoraria/lib/facter
vim /etc/puppetlabs/code/environments/test/modules/zonahoraria/lib/facter/zonahoraria.rb

 Fact

Facter.add(:zonahoraria) do
  setcode do
    if File.exists? '/usr/local/etc/timezone'
      zone = File.readlines('/usr/local/etc/timezone')
      zone[0]
    end
  end
end

 Cambiando el fact en el manifiesto

Con la siguiente línea reemplazmos la variable de facter timezone por la que hemos creado, zonahoraria:

find modules/zonahoraria/manifests/init.pp | xargs perl -pi -e 's/timezone/zonahoraria/g'

 Actualizando el parámetro en el agente

Pedimos el manifiesto al master:

/opt/puppetlabs/bin puppet agent -t

 Por ultimo

Comprobamos que el parámetro leído del archivo se haya actualizado:

cat /etc/php/7.0/fpm/php.ini | grep timezone
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
;date.timezone =
date.timezone = Europe/Madrid

Esta es la idea básica de declaración de facts en Puppet. Por supuesto, hay mucho más. Una lista completa de los facts del sistema pueden obtenserse con el comando /opt/puppetlabs/bin/facter

Configuración de Puppet en Debian Jessie y creación de un módulo

Por domingo, 19 febrero, 2017 0 , Permalink

Cuando tienes hacer un despliegue en muchos servidores o automatizar tareas repetitivas, Puppet es una gran opción. En base a manifiestos declarativos, Puppet se asegura de que existan recursos, los crea sin aún no existen y los utiliza. A diferencia de muchos otros lenguajes, en los manifiestos no se definen los procesos para aprovechar los recursos, sino que se describe «lo que debe haber» y Puppet se encarga de hacerlo realidad. Así ha sido dicho y así será hecho.

Continuar Leyendo…

Automatización de backups de WordPress

Por jueves, 1 diciembre, 2016 0 , Permalink

En este post vamos a seguir con la serie automatización para ver como realizar un backup de la instalación de WordPress. Conviene realizar backups de manera periódica, por lo que seria conveniente planificar con qué frecuencia se realizan, para automatizar también la ejecución del script de backup. Quiero recordar que estos scripts de la serie van en conjunto, es decir, que hay que tener instalado WordPress con el script del primer post de esta serie, que podéis leer aquí.

La copia de seguridad será almacenada en un directorio de un servidor remoto, los cuales serán definidos en un archivo junto con otros parámetros.

Usaremos algunas funciones que ya fueron explicadas en el post sobre el script de la desinstalación con los mismo objetivos:

  • iferror. Utilizada para detener la ejecución el script con un mensaje diciendo en qué parte de la ejecución ha fallado
  • read_root_path.  Utilizada para definir el directorio root de la instalación que respaldamos.
  • get_config_parameters. Utilizada para definir los parámetros para acceder a la base de dato.

Para leer estas tres funciones podéis consultar el post de la desinstalación o visitar mi repositorio en Github desde aquí.

Para la ejecución de este script necesitaremos un archivo con los siguientes parámetros:

  • Nombre de dominio de la instalación a respaldar.
  • Usuario remoto de backup. El usuario que tendrá permisos para escribir en el directorio de almacenamiento.
  • IP o FQDN del host remoto.
  • Directorio remoto para almacenar la copia de respaldo, que será una copia con fecha de la base de datos y archivos.
domain=torestore.org
backup_user=gustavo	
backup_host=192.168.1.254
backup_remote_dir=backup

La primera función específica de este script es mysql_dump. Esta función utiliza los datos leídos con get_config_parameters para acceder a la base de datos con las tablas de la instalación de WordPress y envía una copia al directorio definido en el archivo de parámetros mediante una conexión ssh. El script crea un directorio con nombre igual al nombre de dominio de la instalación en el host remoto de backups:

# function: mysql_dump
# Produces a backup of full database in a tar.gz file

function mysql_dump {
  ssh $backup_user@$backup_host "mkdir -p $backup_remote_dir/$domain"
  mysqldump --user $db_user --password=$db_password  \
  $db_name | gzip -c | ssh $backup_user@$backup_host "cat > \
  $backup_remote_dir/$domain/$(date +%Y%m%d)$db_name.sql.gz" \
  || iferror "Backup for database named $db_name has failed" \
  && wp_simple_backup_files;
}

La función wp_simple_backup_files realiza una copia de todo el directorio raíz de WordPress. «Todo» quiere decir que también hace un respaldo del directorio upload, dónde están las imágenes y otros archivos que pueden tener un gran peso. Los archivos son comprimidos y empaquetados con una nomenclatura que incluye la fecha en el nombre del archivo. La copia será enviada con el comando scp al host remoto:

# function: wp_simple_backup_files
# Produces a tar.gz to a remote host

function wp_simple_backup_files {
  wp_targz="/tmp/$(date +%Y%m%d)$domain.tar.gz";
  if [[ -d $wp_path ]]; then
    tar czfp $wp_targz -C $wp_path . \
    && scp $wp_targz $backup_user@$backup_host:$backup_remote_dir/$domain/. \
    && rm -f $targz_file \
    || iferror "Backup file not sended to $backup_host"
  fi
}

La función nginx_site_backup hace una copia del servidor virtual de Nginx que usa la instalación de WordPress y la envía al directorio del backup:

# function: nginx_site_backup
# Produces a file with Nginx configurarion of the given server

function nginx_site_backup {
  if [[ -e /etc/nginx/sites-enabled/$domain.conf ]]; then
    scp /etc/nginx/sites-enabled/$domain.conf \
    $backup_user@$backup_host:$backup_remote_dir/$domain/.
  fi
}

Como en scripts anteriores de la serie, el script de backup también comprueba que lo haya ejecutado el usuario root y que exista el archivo de parámetros:

if [[ -e ./params/backup.conf ]];then
 		source ./params/backup.conf;
else
    iferror "First you need configure parameters";
fi

if [ "$(id -u)" != "0" ]; then
  echo "This script must be run as root" 1>&2
  exit 1
fi

La funcion start, que es llamada para iniciar el programa:

function start {
 read_root_path \
 && get_config_parameters \
 && mysql_dump \
 && nginx_site_backup
}

El script que he explicado en este post puede lanzarse desde cron para planificar la ejecución de las copias de respaldo. En el siguiente post veremos como restaurar la copia de seguridad. Al haber copiado el blog entero y la configuración servidor virtual, para restaurar la copia no habrá que instalar necesariamente otro WordPress y ambos scripts pueden ser muy útiles para realizar migraciones. Aunque todo eso lo veremos en el próximo post.

Hasta entonces.

Automatización de la desinstalación de WordPress

Por miércoles, 23 noviembre, 2016 0 , Permalink

El segundo post de la serie sobre automatización trata sobre la desinstalación de un servidor WordPress que fue instalado con el script que explicamos en el post anterior de la serie. Con otras instalaciones es muy probable que este script de desinstalación no funcione sin modificaciones.

El script de desinstalación no utiliza ningún archivo de configuración. Tan solo requiere un parámetro al lanzar el script, el cual debe ser el nombre del dominio del servidor que vamos a desinstalar. El script funciona en local.

Como en el script anterior, tenemos las función iferror que, en caso de que sea llamada, sale de script con un mensaje.

# function: iferror
# produces an exit code 1 with message
function iferror {
  if [[ $? -eq 1 ]]; then
    echo $1; exit 1;
  fi
}

Justo al iniciarse la ejecución, comprueba que el administrador haya introducido un parámetro.

if [[ $# -eq 0 ]]; then
  echo "You must enter a FQDN as parameter. \
  Example: ./uninstall_wordpress.sh yourdomain.org"; exit 1;
else
  domain=$1
fi

Esta comprobación aún requiere mejoras. Si el parámetro no es un nombre de dominio, el script terminará su ejecución antes de hacer modificación alguna, cuando no encuentre el dominio para ser desinstalado.

A continuación, comprueba que el usuario que ha lanzado el script sea root.

if [ "$(id -u)" != "0" ]; then
  echo "This script must be run as root" 1>&2
  exit 1
fi

A partir del nombre de dominio introducido como parámetro, el script encuentra el directorio dónde WordPress está instalado:

# function: read_root_path
# Read root path from Nginx server

function read_root_path {
    if [[ -e /etc/nginx/sites-available/$domain.conf ]];then
          wp_path=$(grep -E "root.*$domain" /etc/nginx/sites-available/$domain.conf \
          | awk -F' ' '{print substr($2, 1, length($2)-1)}');
    else
         iferror "Site is not available";
    fi
}

Para ello lee el archivo de configuración del servidor en Ngnix y almacena el directorio en la variable wp_path

Conociendo la ruta en la que se encuentra la instalación, es posible leer el archivo wp-config.php adecuado.

# function: get_config_parameters
# Read wp-config.php to get parameters needed to uninstall

function get_config_parameters {
  wp_cnf=$wp_path/wp-config.php

  db_name=$(grep "DB_NAME" $wp_cnf | awk -F"'" '{print $4}')
  db_user=$(grep "DB_USER" $wp_cnf | awk -F"'" '{print $4}')
  db_password=$(grep "DB_PASSWORD" $wp_cnf | awk -F"'" '{print $4}')
  db_host=$(grep "DB_PASSWORD" $wp_cnf | awk -F"'" '{print $4}')
  if [[ $db_host == '' ]]; then
      db_host=localhost
  fi
}

Define las variables con el nombre de la base de datos, junto usuario y password de la misma, además del host en la que está funcionando.

Con esos datos, podemos proceder. Siguiente paso, el script elimina la base de datos:

# function: mysql_remove_database
# Remove database from mysql

function mysql_remove_database {
   SQL="drop database if exists $db_name;";
   mysql -u$db_user -p$db_password -e "$SQL" || iferror "Database not removed";
}

Ejecuta una sentencia Mysql con las variables que hemos definido antes. Esta función eliminará la base de datos y no será posible recuperarla sin no hay alguna copia en algún otro lugar. En el siguiente post de la serie, publicaré un script para automatizar backups de todo el sitio y enviarlos a un host remoto.

La siguiente función, elimina el directorio raíz del sitio de WordPress:

# function: remove_root_path
# remove path of WordPress installation

function remove_root_path {
  if [[ -d $wp_path ]]; then
    rm -rf $wp_path;
  else
    iferror "Root path does not exists"
  fi
}

La siguientes dos funciones, eliminan el servidor de Nginx. La primera de ellas deshabilita el servidor borrando el enlace simbólico; la segunda, elimina la disponibilidad del servidor.

# function: nginx_disable_site
# disable site from sites-enabled

function nginx_disable_site {
  if [[ -e /etc/nginx/sites-enabled/$domain.conf ]]; then
      rm -f /etc/nginx/sites-enabled/$domain.conf;
      systemctl reload nginx;
  else
      iferror "Site is not enabled"
  fi
}

#function nginx_remove_site
# remove server from sites-available

function nginx_remove_site {
  if [[ -e /etc/nginx/sites-available/$domain.conf ]]; then
      rm -f /etc/nginx/sites-available/$domain.conf;
  else
      iferror "Site is not available "
  fi
}

La función start ejecuta la funciones en serie.  Una función a las que llame esta función no se ejecuta si la función anterior no se ha ejecutado con éxito, a excepción de la primera de ellas.

function start {
  read_root_path \
  && get_config_parameters \
  && mysql_remove_database \
  && remove_root_path \
  && nginx_disable_site \
  && nginx_remove_site
}

Por último, tan sólo hay que llamar a la función start para ejecutar todo el script.

Todos los scripts de esta serie relacionados con WordPress están en mi github.

Saludos 🙂

Automatización de la instalación de WordPress

Por martes, 15 noviembre, 2016 0 , Permalink

Éste será el primero de una serie de posts sobre automatización en varios lenguajes de scripting. En este post expongo una automatización para instalación de WordPress, con un backend de MariaDB y PHP 7.0, en una distribución Debian Jessie. En próximas entregas desgranaré scripts de desinstalación, backup y restauración de la instalación realizada con este script.

Continuar Leyendo…