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.