Desde hace varias semanas un par de meses, tenía un sitio listo para ser deployado, pero necesitaba cambiarle la versión de PHP (no era condición, pero era lo apropiado) y de paso parecía una buena idea deshacerme del servidor que está usando un webserver local con varios virtual hosts. Todo con instalación nativa.

A pesar de tener todos los pasos y comandos documentados para hacer copy&paste, me daba muchísima pereza comenzar un servidor de cero y tener que configurar todo otra vez (aún sabiendo que no me tomaría más de una hora).

De alguna manera, un poco de «Elijo a una persona perezosa para hacer un trabajo difícil, porque una persona perezosa encontrará una manera fácil de hacerlo» y otro poco de «Hazlo funcionar, luego hazlo hermoso, y luego, si realmente es necesario, hazlo rápido» se cruzaron en mi camino. Y esa mezcla llevó a este post.

La necesidad antes mencionada junto a la poca voluntad por hacerlo, se convirtió en un playbook que, independientemente del proveedor que use, con sólo ejecutar un comando en mi bastión, me prepare un servidor desde 0.

Quizás remontarse a este post es un buen comienzo, ya que allí comenzaba a compartir cómo fue parte de mi paso de instalaciones nativas a Docker y Traefik (incluso, ahí puede verse cómo configurar, mínimamente, el servidor host).

Repasemos entonces cómo espero que quede finalmente el servidor:

  • Un servidor host con Debian (lo más liviano posible, con un set de configuraciones mínimas y algunas herramientas de monitoreo menor)
  • Docker.
  • Traefik ya listo para actuar como Proxy reverso.
  • Un proyecto de un sitio web con su propio docker-compose y todas sus definiciones y necesidades.

Lo primero que voy a recordar es cómo se ejecuta un playbook (porque hay una parámetro extra que implicará una acción al momento de crear el playbook en si mismo).

ansible-playbook -i "ip_del_servidor," -u usaurio archivo-de-mi-playbook.yml

Esta sería la forma más básica de ejecución. Algunas notas básicas:

  • El parámetro -i es para el inventario, que podría ser un archivo o la dirección IP o el dominio (no se olviden de la , al final de la dirección IP)
  • Indicar -u es para indicar el nombre de usuario remoto (si corresponde, sino se usa el mismo que está ejecutando en playbook en el entorno local)
  • El path/nombre del archivo a ejecutar.

Antes de comenzar con el ejemplo del playbook en si, vamos a abrir un paréntesis para hablar de Ansible Vault.

Ansible Vault nos permite cifrar datos sensibles directamente en los archivos de configuración. Vamos a usarlo entonces para integrarlo en nuestro playbook para almacenar la contraseña del usuario (o los usuarios) que vayamos a crear de manera segura.

El primer paso será entonces crear un archivo cifrado con la contraseña para ese usuario. Para esto, ejecutamos el siguiente comando (en donde tenemos Ansible instalado, claro):

ansible-vault create secret.yml

Nos va a pedir una contraseña para el archivo (la cual no hay que olvidar) y se va a abrir el editor de texto (posiblemente Vim o Nano) y dentro del archivo vamos a agregar la variable de la contraseña:

password_de_mi_usuario: "password1234"

Al guardar y cerrar el archivo, el mismo quedará cifrado.

Si necesitamos agregar o editar información, podemos usar:

ansible-vault edit secret.yml

Luego de ingresar esa contraseña que no debemos olvidar, podemos cambiar o agregar datos.

Ahora que ya tenemos nuestro vault listo, podemos ir al siguiente paso, el armado del playbook que sabiendo muy poco de mi servidor recién creado, se ocupará de instalarlo y dejarlo listo para ser usado.

Antes que nada, definamos qué vamos a instalar. Ya dijimos que era un servidor Debian, ¿pero qué más y por qué?

  • Vamos a crear un usuario sin privilegios (en general, los proveedores de VPS te permiten asignar una clave para el usuario root o copian tu public key, el ejemplo va a asumir el segundo caso).
  • Vamos a configurar el timezone del servidor.
  • Actualizaremos todos los paquetes del sistema operativo.
  • Instalaremos: apache2-utils, ufw, fail2ban, logwatch, unattended-upgrades, git, docker, unzip y openssh-client.
  • Ajustaremos los locales del sistema operativo.
  • Configuraremos UFW.
  • En mi caso, que ya tengo armado un zip con el docker-composer para Traefik casi listo para usar, lo que voy a hacer es copiarlo al servidor para ubicarlo y configurarlo tanto como em sea posible.
  • Ajustamos el hostname del servidor (para el TOC).
  • Creo la red externa para Docker que usará Traefik y los containers que vaya creando.

El playbook, al que llamaremos mi-playbook-para-crear-servidor.yml, sería algo más o menos así:

---
- name: Instalar y configurar mi servidor
  hosts: all
  become: true
  vars_files:
    - secrets.yml
  tasks:
    - name: Creamos usuario "mi_usuario" usando la contraseña encriptada
      user:
        name: mi_usuario
        shell: /bin/bash
        create_home: yes
        password: "{{ password_de_mi_usuario | password_hash('sha512') }}"
        groups: sudo
        append: yes

    - name: Nos aseguramos que existe el directorio ~/.ssh para "mi_usuario"
      file:
        path: "/home/mi_usuario/.ssh"
        state: directory
        mode: "0700"

    - name: Copiamos la clave pública para "mi_usuario"
      copy:
        src: "./plantillas/mi_usuario_authorized_keys"
        dest: "/home/mi_usuario/.ssh/authorized_keys"
        owner: mi_usuario
        group: mi_usuario
        mode: "0440"

    - name: Definimos el timezone del servidor
      timezone:
        name: "America/Argentina/Buenos_Aires"

    - name: Actualizamos los paquetes de la distribución a la última versión
      apt:
        update_cache: yes
        upgrade: dist

    - name: Set de herramientas que usaremos en nuestro host
      apt:
        name:
          - apache2-utils
          - ufw
          - fail2ban
          - logwatch
          - unattended-upgrades
          - git
          - docker.io
          - docker-compose
          - unzip
          - openssh-client
        state: present

    - name: Configuramos los locales para el sistema operativo
      community.general.locale_gen:
        name:
          - en_US.UTF-8
          - es_AR.UTF-8
        state: present

    - name: Configuración general del firewall
      ufw:
        state: enabled
        default: deny
        direction: incoming

    - name: Permitimos trafico en puertos 22, 80 y 443
      ufw:
        rule: allow
        port: "{{ item }}"
        proto: any
      loop:
        - "22"
        - "80"
        - "443"

    - name: Configuramos unattended-upgrades con un archivo de configuración ya armado
      copy:
        src: "./plantillas/unattended_upgrades_config"
        dest: "/etc/apt/apt.conf.d/50unattended-upgrades"
        owner: root
        group: root
        mode: "0644"

    - name: Creamos un archivo vació para el cron de unattended-upgrades
      file:
        path: "/etc/apt/apt.conf.d/20auto-upgrades"

    - name: Configuramos el cron de unattended-upgrades con un archivo de configuración ya armado
      copy:
        src: "./plantillas/unattended_upgrades_cron"
        dest: "/etc/apt/apt.conf.d/20auto-upgrades"
        owner: root
        group: root
        mode: "0644"

    - name: Copiamos el archivo de configuración de logwatch con un archivo local
      copy:
        src: "./plantillas/logwatch_config"
        dest: "/usr/share/logwatch/default.conf/logwatch.conf"
        owner: root
        group: root
        mode: "0644"

    - name: Configuramos el cron de logwatch con un archivo local
      copy:
        src: "./plantillas/logwatch_cron"
        dest: "/etc/cron.daily/00logwatch"
        owner: root
        group: root
        mode: "0644"

    - name: Configuramos el servicio SSH
      lineinfile:
        path: "{{ ssh_config_file }}"
        regex: "{{ item.regex }}"
        line: "{{ item.line }}"
        state: present
      loop:
        - { regex: "^#?PermitRootLogin", line: "PermitRootLogin no" }
        - { regex: "^#?PasswordAuthentication", line: "PasswordAuthentication no" }
        - { regex: "^#?PubkeyAuthentication", line: "PubkeyAuthentication yes" }

    - name: Reiniciamos SSH
      service:
        name: ssh
        state: restarted

    - name: Copiamos el zip de Traefik local
      copy:
        src: "./archivos/traefik.zip"
        dest: "/directorio/en/donde/quieran/usar/traefik.zip"
        owner: root
        group: root
        mode: "0644"

    - name: Unzip Traefik
      ansible.builtin.unarchive:
        src: "/directorio/en/donde/quieran/usar/traefik.zip"
        dest: "/directorio/en/donde/quieran/usar"
        remote_src: yes

    - name: Creamos un directorio vacío para los logs de acceso de Traefik
      file:
        path: "/directorio/en/donde/quieran/usar/traefik/log"
        state: directory
        mode: "0644"

    - name: Creamos un log de acceso vacío para Traefik
      file:
        path: "/directorio/en/donde/quieran/usar/traefik/log/access.log"
        state: touch
        mode: "0644"

    - name: Creamos un archivo vacío para las reglas de Trafik dentro de Fail2Ban
      file:
        path: "/etc/fail2ban/jail.d/traefik.conf"
        state: touch
        mode: "0644"

    - name: Copiamos mi plantilla de configuración para Traefik y Fail2Ban
      copy:
        src: "./plantillas/fail2ban-traefik"
        dest: "/etc/fail2ban/jail.d/traefik.conf"
        owner: root
        group: root
        mode: "0644"

    - name: Borramos el archivo zip de Traefik que subimos antes
      ansible.builtin.file:
        path: "/directorio/en/donde/quieran/usar/traefik.zip"
        state: absent

    - name: Reiniciamos el servicio Fail2Ban
      service:
        name: fail2ban
        state: restarted

    - name: Definimos el hostname del servidor
      hostname:
        name: "{{ new_hostname }}"

    - name: Agregamos el hostname en /etc/hosts
      lineinfile:
        path: /etc/hosts
        line: "127.0.1.1 {{ new_hostname }}"
        state: present

    - name: Creamos la red "web" que usará Traefik
      community.docker.docker_network:
        name: web

Si el playbook hace lo que necesitamos, ahora nos tocará ejecutarlo.

ansible-playbook -i "ip_del_servidor," -u root --ask-vault-pass mi-playbook-para-crear-servidor.yml -e "new_hostname=mi-servidor"

Suponiendo que todo estuvo bien, ahora nos conectamos al servidor.

ssh mi_usuario@ip_del_servidor

Como hasta aquí todo es configuración por defecto, necesitamos convertirnos en root y luego iniciamos Traefik.

cd /directorio/en/donde/quieran/usar/traefik
docker-compose -p traefik up -d

En este momento, deberíamos tener:

  • El servidor host medianamente configurado, con los paquetes necesarios y algunas medidas de seguridad.
  • Docker listo para ser usado.
  • Traefik ya configurado.

En general lo que he hecho aquí fue automatizar un poco lo mostrado aquí y aquí. El siguiente paso será copiar el docker compose y el código de un proyecto cualquiera e inicializarlo.

Este playbook no es una versión definitiva. El que yo uso realmente tiene algunas otras cosas específicas y muchos de los valores los inyecto con un archivo vars.yml, pero no quería sumar tanto ruido en la explicación. Mi sugerencia es que hagan tantas pruebas como necesiten para afinar los servidores a lo que necesitan.

Actualmente, con un playbook un poquito (no mucho) más extenso, el tiempo total para llegar desde recién creado a Traefik inicializado, está en seis minutos usando las VPS más pequeñas. A a partir de ese momento, mi nuevo servidor está listo para recibir containers y proyectos.

Si bien no es parte de este post, hasta aquí esto se convierte en un genérico para los distintos proyectos, ya que lo instalado no depende de los recursos y tampoco está atado a las particularidades del proyecto.

Luego viene el incializar cada uno de los proyectos, sitios o aplicaciones. Para esto, así como versionamos el código del proyecto, versiono el docker-compose de cada proyecto, y usando un archivo de configuración, el mismo docker-compose me sirve para los distintos entornos (Producción, Staging, Development o Local).

Normalmente este proceso toma otros cinco minutos (diez en proyectos complejos con varios containers y configuraciones).

En resúmen:

  • Podemos crear servidores nuevos en cinco o seis minutos que ya dispongan de Docker y Traefik funcionando (ejecutando sólo un comando, quizás dos)
  • En otros cinco a diez minutos podemos tener nuestro sitio funcionando.
  • Sumamos flexibilidad para cambiar infraestructura o actualizarla.
  • Mejoramos la calidad y estandarizamos nuestra infraestructura, porque ahora será siempre la misma.

Además, y esto ha sido siempre parte de la charla en el podcast, es una forma de traer ciertas prácticas al mundo SMB (o MSME) sin que signifique un incremento de costos.

Unite a la lista de suscriptores

Una vez por mes vas a recibir un mail con contenido que se relaciona con lo que vemos en el blog, que extiende o anticipa lo que hacemos en Twitch, y que también suele incluir anécdotas del MundoReal® y algún que otro link.

Es gratis, no tiene publicidad y con el double opt-in de Mailchimp.