19 Deployent and hosting

En esta sección final del libro vamos a ver cómo desplegar nuestra aplicación de API en un servidor de producción y exponerla en Internet.

Cada proyecto y equipo de proyecto tendrá diferentes necesidades técnicas y empresariales en términos de alojamiento y despliegue, por lo que es imposible establecer un enfoque único aquí.

Para hacer que el contenido de esta sección sea lo más aplicable y portable posible, nos centraremos en alojar la aplicación en un servidor Linux autoadministrado (algo proporcionado por una miríada de empresas de alojamiento en todo el mundo) y utilizar herramientas estándar de Linux para gestionar la configuración y el despliegue del servidor.

También automatizaremos el proceso de configuración y despliegue del servidor tanto como sea posible, para que sea fácil realizar despliegues continuos y sea posible replicar el servidor nuevamente en el futuro si es necesario.

Si planeas seguir los pasos, utilizaremos Digital Ocean como proveedor de alojamiento en este libro. Digital Ocean no es gratuito, pero tiene un buen valor y el costo de ejecutar un servidor comienza en $4 USD por mes. Si no quieres usar Digital Ocean, deberías poder seguir básicamente el mismo enfoque que describimos aquí con cualquier otro proveedor de alojamiento Linux.

En términos de infraestructura y arquitectura, ejecutaremos todo en un único servidor Ubuntu Linux. Nuestra pila consistirá en una base de datos PostgreSQL y el binario ejecutable para nuestra API de Greenlight, operando de manera muy similar a como hemos visto hasta ahora en este libro. Pero además de esto, también ejecutaremos Caddy como un proxy inverso delante de la API de Greenlight. El uso de Caddy tiene un par de beneficios. Manejará y terminará automáticamente las conexiones HTTPS por nosotros, incluyendo la generación y gestión automática de certificados TLS a través de Let’s Encrypt y ZeroSSL, y también podemos usar Caddy para restringir fácilmente el acceso a Internet a nuestro punto final de métricas.

En esta sección aprenderás cómo:

  1. Comisionar un servidor Ubuntu Linux en Digital Ocean para alojar tu aplicación.
  2. Automatizar la configuración del servidor, incluyendo la creación de cuentas de usuario, configuración del firewall e instalación de software necesario.
  3. Automatizar el proceso de actualización de tu aplicación y despliegue de cambios en el servidor.
  4. Ejecutar tu aplicación como un servicio en segundo plano utilizando systemd, como un usuario no root.
  5. Utilizar Caddy como un proxy inverso delante de tu aplicación para gestionar automáticamente certificados TLS y manejar conexiones HTTPS.

20.1 Creando un Droplet en Digial Ocean

Lo primero que necesitamos hacer es comisionar un servidor en Digital Ocean para alojar nuestra aplicación.

Estrictamente hablando, lo que vamos a comisionar es en realidad una máquina virtual conocida en la terminología de Digital Ocean como un droplet.

Si deseas seguir esta etapa del libro, deberás registrarte para obtener una cuenta en Digital Ocean si aún no tienes una. Como parte del proceso de registro, se te pedirá que confirmes tu dirección de correo electrónico y luego agregues un mínimo de $5 USD de crédito prepagado a tu cuenta usando una tarjeta de crédito/débito o PayPal.

Una vez que hayas completado el registro y agregado el crédito, deberías encontrarte con el panel de control de tu cuenta.

20.1 Creando una clave SSH

Para iniciar sesión en los droplets de tu cuenta de Digital Ocean, necesitarás un par de claves SSH.

Sugerencia: Si no estás familiarizado con SSH, claves SSH o criptografía de clave pública en general, te recomiendo leer la primera mitad de esta guía para obtener una visión general antes de continuar.

Si ya tienes un par de claves SSH que estás dispuesto a utilizar para este propósito, entonces genial, y puedes pasar a la siguiente sección.

Pero si no tienes una, necesitarás crear un par de claves utilizando el comando ssh-keygen en tu máquina local. Similar a esto:

$ ssh-keygen -t rsa -b 4096 -C "greenlight@greenlight.alexedwards.net" -f $HOME/.ssh/id_rsa_greenlight
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/alex/.ssh/id_rsa_greenlight
Your public key has been saved in /home/alex/.ssh/id_rsa_greenlight.pub
The key fingerprint is:
SHA256:MXASjIyE1p2BAGZ70zkUV058rA65hm3sxdIcnWLGkwg greenlight@greenlight.alexedwards.net
The key's randomart image is:
+---[RSA 4096]----+
|o*+oo*Bo+o.
|+.oo=E++o. o|
|
|.. o +. *.= .|
|. . .+ % o|
|+ S +|
|. B *|
|+ o|
|.|
|
|
+----[SHA256]-----+

Esto generará dos nuevos archivos en tu carpeta $HOME/.ssh:

  • El archivo $HOME/.ssh/id_rsa_greenlight contiene tu clave privada. Asegúrate de mantenerla segura, porque cualquiera que tenga acceso a ella podrá suplantarte.

  • El archivo $HOME/.ssh/id_rsa_greenlight.pub contiene tu clave pública. Subiremos una copia de esta clave pública a Digital Ocean. Si abres tu archivo de clave pública en un editor de texto, deberías ver que el contenido se parece a esto (se han añadido saltos de línea para facilitar la lectura):

Si abres tu archivo de clave pública en un editor de texto, deberías ver que el contenido se parece a esto (se han añadido saltos de línea para facilitar la lectura):

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDBheUdwyzUt056EsvUidpaGqL3zodDAffHbVVPPN7AJal5/oL6
hzmpPoCGIZueU3Fra2BPzrVtBTsNLOm0UwyQ3G8D474ETsWqlgtU3M3DBvdeI0sAaQdGxH8SkgUGswRUPNNzVG3V
xvu5aOludfZ0J1kKEkS1PzWXRll2YoKlzSO42Ne0Gzo++ZdbQWl0Y/C0sLb2sBBviIxXHU8dXmp3823yUErqkWrF
ZGGBhAco9t18gUe6MLei1+AyK+VHnRbCYvStrId7qExEs2dPzCmaEec01wCnLJ6LaRYZQnFpBRuzIZ9dTwwsJH+T
cXGo87x8MRnGY6nKNVoz8lupbSyjxkHw3PBTfkelJh+tNKiFzxs8J8WiHDfJKzrKDDQVUGbE3TYZXddGSxvi+rv0
Sfrf85zgvPjRVa2E6tjTl6nD8CnC+3wlU/01gRjVxVtPx7B9n51f+k8n2vMm8UozAv6+YruE1zZoHRHw9IvPCEy3
5B4l6GJxWzAqzjTns7kJR3Qk+xzcu2jOAehc+Do8MMx+xegzOzlgRY3mbPx8jbB3L1WmjNF6vV2BrJR/NxKoRgTf
nAQA44JaevaG4+KpbVZvkvSNoaI8uP6z8b5AptUHz/QO9Gc5M+n2EAPFjv/lNMN+0g5ZmMH8n6NpBXzP9Qmgujgn
hsGP+GmI4ZgvuUgjTQ== greenlight@greenlight.nahueldev23.net

Y si ejecutas el comando ssh-add -l, deberías ver tu nueva clave SSH en la salida, similar a esto:

$ ssh-add -l
4096 SHA256:MXASjIyE1p2BAGZ70zkUV058rA65hm3sxdIcnWLGkwg greenlight@greenlight.alexedwards.net (RSA)

Si no ves tu clave en la lista, por favor agrégala a tu agente SSH de la siguiente manera:

$ ssh-add $HOME/.ssh/id_rsa_greenlight
Enter passphrase for /home/alex/.ssh/id_rsa_greenlight:
Identity added: /home/nahuel/.ssh/id_rsa_greenlight (greenlight@greenlight.nahueldev23.net)

20.1 Agregando la llave SSH a Digital Ocean.

Ahora que tienes un par de claves SSH que puedes utilizar, regresa a tu panel de control de Digital Ocean y ve a la pestaña Configuración › Seguridad.

Haz clic en el botón “Agregar clave SSH”, luego en la ventana emergente que aparece, pega el contenido de texto de tu archivo de clave pública $HOME/.ssh/id_rsa_greenlight.pub, dale un nombre memorable y envía el formulario.

La pantalla debería actualizar para confirmar que tu clave SSH ha sido agregada exitosamente.

20.1 Creando un droplet

Ahora que tienes una clave SSH válida agregada a tu cuenta, es hora de crear realmente un droplet.

Hay un par de formas de hacer esto. Es posible hacerlo de forma programática a través de la API de Digital Ocean o utilizando la herramienta de línea de comandos oficial, y si necesitas crear o administrar muchos servidores, te recomiendo que utilices estas opciones.

O alternativamente, es posible crear un droplet manualmente a través de tu panel de control en el sitio web de Digital Ocean. Este es el enfoque que tomaremos en este libro, en parte porque es lo suficientemente simple como para hacerlo de manera puntual, y en parte porque ayuda a dar una visión general de las configuraciones de droplet disponibles si no has utilizado Digital Ocean antes.

Ve adelante y haz clic en el botón verde “Crear” en la esquina superior derecha y selecciona “Droplets” desde el menú desplegable:

Esto te llevará a la página de opciones para crear un nuevo droplet. Lo primero que debes elegir es el centro de datos donde se alojará físicamente tu droplet. Voy a elegir Frankfurt, pero si lo prefieres, siéntete libre de seleccionar una ubicación alternativa.

Lo siguiente que debes elegir es el sistema operativo para tu droplet. Si estás siguiendo, por favor selecciona Ubuntu 22.04 (LTS) x64.

Luego, necesitas elegir un plan que coincida con las especificaciones técnicas que necesitas para el droplet. En este caso, seleccionaremos el plan Regular Básico a $4/mes, que nos dará una máquina virtual con 512MB de RAM, 10GB de espacio en disco y 500GB de transferencia de datos salientes cada mes (la transferencia de datos entrantes no tiene restricciones).

La siguiente opción nos permite agregar almacenamiento en bloques al droplet. Esto es esencialmente un volumen de almacenamiento independiente del droplet que actúa como un disco duro local y puede ser movido entre diferentes droplets. No es algo que necesitemos en este momento, así que puedes omitir esta sección.

En la sección Método de Autenticación, asegúrate de que se haya seleccionado Claves SSH como método de autenticación y de que la clave SSH que acabas de cargar esté marcada.

Después de eso, podemos seleccionar algunas características adicionales para nuestro droplet. En nuestro caso, seleccionaremos Monitorización, lo que te permitirá más tarde ver gráficos de varias estadísticas del droplet (como el uso de CPU, memoria y disco) en tu panel de control de Digital Ocean, y también puedes configurar alertas si el uso de recursos excede cierto umbral.

También puedes optar por pagar un cargo adicional del 20% para habilitar copias de seguridad automáticas del droplet. Si seleccionas esta opción, entonces Digital Ocean tomará una ‘instantánea’ de tu droplet una vez a la semana y la almacenará durante un período de 4 semanas. Luego puedes restaurar un droplet a su estado instantáneo a través del panel de control si alguna vez lo necesitas. Es completamente opcional si habilitar o no las copias de seguridad, pero es una red de seguridad simple y sin complicaciones.

Luego llegamos a las opciones de configuración finales.

Solo necesitamos un droplet en este momento, así que puedes dejar eso como predeterminado.

También deberías agregar un nombre de host para el droplet. Entre otras cosas, el nombre de host se utiliza como el identificador principal del droplet en el panel de control de Digital Ocean, y también es lo que verás cuando te conectes por SSH al droplet más tarde para administrarlo. Por lo tanto, debes elegir un nombre que sea sensato y fácil de reconocer. Voy a usar el nombre de host greenlight-production, pero si prefieres usar algo diferente, siéntete libre de hacerlo.

Agregar etiquetas a tu droplet es completamente opcional, pero si trabajas mucho con Digital Ocean, pueden ser una forma útil de ayudar a filtrar y administrar droplets. Aquí usaré las etiquetas greenlight y production.

Una vez que todo esté configurado, ve y haz clic en el botón Crear Droplet en la parte inferior de la pantalla. Deberías ver una barra de progreso mientras el droplet se está configurando para ti, y después de uno o dos minutos, el droplet debería estar en línea y listo para usar.

Lo más importante en este momento es tomar nota de la dirección IP del droplet, que en mi caso es 161.35.71.158. Si lo deseas, también puedes hacer clic en el nombre de host del droplet para ver información más detallada sobre el droplet (incluidas las estadísticas de monitoreo) y realizar cualquier cambio adicional de configuración y gestión si es necesario.

¡Bien, ahora que el droplet está configurado, es el momento de la verdad! Abre una nueva ventana de terminal e intenta conectarte al droplet a través de SSH como usuario root, utilizando la dirección IP del droplet. Así…

$ ssh root@161.35.71.158
The authenticity of host '161.35.71.158 (161.35.71.158)' can't be established.
ED25519 key fingerprint is SHA256:pBVp+W/Sb/BZkQy5JnsGQ0+QOr6clTtB3CFoEOFPKTk.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '161.35.71.158' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-50-generic x86_64)
* Documentation:https://help.ubuntu.com
* Management:https://landscape.canonical.com
* Support:https://ubuntu.com/advantage
System information as of Thu Feb 23 15:17:11 UTC 2023
System load:0.05615234375Users logged in:
Usage of /:16.8% of 9.51GBIPv4 address for eth0: 161.35.71.158
0
Memory usage: 43%IPv4 address for eth0: 10.19.0.5
Swap usage:0%IPv4 address for eth1: 10.114.0.2
Processes:105
112 updates can be applied immediately.
66 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law
root@greenlight-production:~#

Nota: Como es la primera vez que utilizas SSH para conectarte a este droplet, recibirás un mensaje que dice que “la autenticidad del host no se puede establecer”. Esto está bien en este caso, simplemente escribe yes para continuar. Si tu clave SSH está protegida con una frase de paso, es posible que también se te pida que la ingreses.

Genial, parece que todo está funcionando bien. Nuestro droplet de Ubuntu Linux está en funcionamiento y hemos podido conectarnos exitosamente a él como usuario root a través de SSH.

Puedes seguir adelante y escribir exit para terminar la conexión SSH y regresar al terminal en tu máquina local, así:

root@greenlight-production:~# exit
logout
Connection to 161.35.71.158 closed.

20.2 Configuración de servidor e instalación de software

Ahora que nuestro droplet de Ubuntu Linux ha sido comisionado con éxito, necesitamos hacer algunas tareas de mantenimiento para asegurar el servidor y prepararlo para su uso. En lugar de hacer esto manualmente, en este capítulo vamos a crear un script reutilizable para automatizar estas tareas de configuración.

Nota: En este capítulo utilizaremos un script bash regular para realizar las tareas de configuración, pero si te sientes cómodo utilizando una herramienta de gestión de configuraciones como Ansible, por favor, siéntete libre de implementar el mismo proceso de configuración usando eso en su lugar.

A nivel alto, queremos que nuestro script de configuración haga lo siguiente:

  1. Actualizar todos los paquetes en el servidor, incluyendo la aplicación de actualizaciones de seguridad.
  2. Establecer la zona horaria del servidor (en mi caso, la configuraré en Europe/Berlin) e instalar soporte para todas las [localidades].
  3. Crear un usuario llamado greenlight en el servidor, que podamos usar para el mantenimiento diario y para ejecutar nuestra aplicación de API (en lugar de utilizar la cuenta de usuario root, que tiene todos los privilegios). También debemos añadir el usuario greenlight al grupo sudo, para que pueda realizar acciones como root si es necesario.
  4. Copiar el directorio $HOME/.ssh del usuario root al directorio home del usuario greenlight. Esto nos permitirá autenticarnos como el usuario greenlight utilizando el mismo par de claves SSH que utilizamos para autenticarnos como el usuario root. También debemos obligar al usuario greenlight a establecer una nueva contraseña la primera vez que inicie sesión.
  5. Configurar los ajustes del cortafuegos para permitir únicamente el tráfico en los puertos 22 (SSH), 80 (HTTP) y 443 (HTTPS). También instalaremos fail2ban para bloquear automáticamente de forma temporal una dirección IP si realiza demasiados intentos de inicio de sesión SSH fallidos.
  6. Instalar PostgreSQL. También crearemos la base de datos y el usuario greenlight, y crearemos una variable de entorno GREENLIGHT_DB_DSN a nivel del sistema para conectarse a la base de datos.
  7. Instalar la herramienta migrate, utilizando los binarios precompilados de GitHub, de la misma manera que hicimos anteriormente en el libro.
  8. Instalar Caddy siguiendo las instrucciones de instalación oficiales para Ubuntu.
  9. Reiniciar el droplet.

Si estás siguiendo, crea una nueva carpeta remote/setup en el directorio de tu proyecto y agrega un archivo de script llamado 01.sh, así:

$ mkdir -p remote/setup
$ touch remote/setup/01.sh

Agrega lo siguiente:

#!/bin/bash
set -eu

# ==================================================================================== #
# VARIABLES
# ==================================================================================== #

# Set the timezone for the server. A full list of available timezones can be found by
# running timedatectl list-timezones.
TIMEZONE=Europe/Berlin

# Set the name of the new user to create.
USERNAME=greenlight

# Prompt to enter a password for the PostgreSQL greenlight user (rather than hard-coding
# a password in this script).
read -p "Enter password for greenlight DB user: " DB_PASSWORD

# Force all output to be presented in en_US for the duration of this script. This avoids
# any "setting locale failed" errors while this script is running, before we have
# installed support for all locales. Do not change this setting!
export LC_ALL=en_US.UTF-8

# ==================================================================================== #
# SCRIPT LOGIC
# ==================================================================================== #

# Enable the "universe" repository.
add-apt-repository --yes universe

# Update all software packages.
apt update

# Set the system timezone and install all locales.
timedatectl set-timezone ${TIMEZONE}
apt --yes install locales-all

# Add the new user (and give them sudo privileges).
useradd --create-home --shell "/bin/bash" --groups sudo "${USERNAME}"

# Force a password to be set for the new user the first time they log in.
passwd --delete "${USERNAME}"
chage --lastday 0 "${USERNAME}"

# Copy the SSH keys from the root user to the new user.
rsync --archive --chown=${USERNAME}:${USERNAME} /root/.ssh /home/${USERNAME}

# Configure the firewall to allow SSH, HTTP and HTTPS traffic.
ufw allow 22
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable

# Install fail2ban.
apt --yes install fail2ban

# Install the migrate CLI tool.
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz
mv migrate.linux-amd64 /usr/local/bin/migrate

# Install PostgreSQL.
apt --yes install postgresql

# Set up the greenlight DB and create a user account with the password entered earlier.
sudo -i -u postgres psql -c "CREATE DATABASE greenlight"
sudo -i -u postgres psql -d greenlight -c "CREATE EXTENSION IF NOT EXISTS citext"
sudo -i -u postgres psql -d greenlight -c "CREATE ROLE greenlight WITH LOGIN PASSWORD '${DB_PASSWORD}'"

# Add a DSN for connecting to the greenlight database to the system-wide environment
# variables in the /etc/environment file.
echo "GREENLIGHT_DB_DSN='postgres://greenlight:${DB_PASSWORD}@localhost/greenlight'" >> /etc/environment

# Install Caddy (see https://caddyserver.com/docs/install#debian-ubuntu-raspbian).
apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt --yes install caddy

# Upgrade all packages. Using the --force-confnew flag means that configuration
# files will be replaced if newer ones are available.
apt --yes -o Dpkg::Options::="--force-confnew" upgrade

echo "Script complete! Rebooting..."
reboot

Bien, ahora ejecutaremos este script en nuestro nuevo droplet de Digital Ocean. Este será un proceso de dos pasos:

  1. Primero necesitamos copiar el script al droplet (lo haremos usando rsync).
  2. Luego necesitamos conectarnos al droplet a través de SSH y ejecutar el script. Adelante y ejecuta el siguiente comando para sincronizar con rsync los contenidos de la carpeta /remote/setup en el directorio de inicio del usuario root en el droplet. ¡Recuerda reemplazar la dirección IP con la tuya!
$ rsync -rP --delete ./remote/setup root@161.35.71.158:/root
sending incremental file list
setup/
setup/01.sh
3,354 100%
0.00kB/s
0:00:00 (xfr#1, to-chk=0/2)

Nota: En este comando rsync, la bandera -r indica que queremos copiar los contenidos de ./remote/setup recursivamente, la bandera -P indica que queremos mostrar el progreso de la transferencia, y la bandera --delete indica que queremos eliminar cualquier archivo superfluo del directorio de destino en el droplet.

Ahora que una copia de nuestro script de configuración está en el droplet, usemos el comando ssh para ejecutar el script en la máquina remota como usuario root. Utilizaremos la bandera -t para forzar la asignación de un pseudoterminal, lo cual es útil cuando se ejecutan programas basados en pantalla en una máquina remota.

Adelante y ejecuta el script, ingresando una contraseña para el usuario greenlight de PostgreSQL, así:

$ ssh -t root@161.35.71.158 "bash /root/setup/01.sh"
Enter password for greenlight DB user: pa55word1234
Adding component(s) 'universe' to all repositories.
Hit:1 https://repos.insights.digitalocean.com/apt/do-agent main InRelease
Hit:2 http://mirrors.digitalocean.com/ubuntu jammy InRelease
Hit:3 http://mirrors.digitalocean.com/ubuntu jammy-updates InRelease
Hit:4 https://repos-droplet.digitalocean.com/apt/droplet-agent main InRelease
Hit:5 http://mirrors.digitalocean.com/ubuntu jammy-backports InRelease
...
Script complete! Rebooting...
Connection to 161.35.71.158 closed by remote host.
Connection to 161.35.71.158 closed.

Importante: Mientras este script se esté ejecutando, recibirás un mensaje que dice: “Hay disponible una nueva versión del archivo de configuración /etc/ssh/sshd_config, pero la versión actual instalada ha sido modificada localmente”. Esto se debe a que Digital Ocean realiza automáticamente algunas adaptaciones al archivo sshd_config, incluida la desactivación del acceso basado en contraseñas cuando estás utilizando claves SSH para autenticación. Por favor, selecciona “mantener la versión local instalada actualmente” y luego <Ok> para continuar.

También podrías ver un mensaje que dice: “Nuevo kernel disponible… No se manejará automáticamente el reinicio del sistema para cargar el nuevo kernel, así que deberías considerar reiniciar”. Por favor, selecciona Ok para continuar, y el script reiniciará automáticamente el servidor.

Después de unos minutos, el script debería completarse correctamente y el droplet se reiniciará, lo que también te desconectará de la conexión SSH.

20.2 conectandonos como el usuario greenlight

Después de esperar un minuto para que se complete el reinicio, intenta conectarte al droplet como el usuario greenlight a través de SSH. Esto debería funcionar correctamente (y se utilizará el par de claves SSH que creaste en el capítulo anterior para autenticar la conexión), pero se te pedirá que establezcas una contraseña.

$ ssh greenlight@161.35.71.158
You are required to change your password immediately (administrator enforced).
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-60-generic x86_64)
* Documentation:https://help.ubuntu.com
* Management:https://landscape.canonical.com
* Support:https://ubuntu.com/advantage
System information as of Thu Feb 23 16:46:00 CET 2023
System load:
0.0
Users logged in:
0
Usage of /:
25.7% of 9.51GB
Memory usage: 41%IPv4 address for eth0: 161.35.71.158
IPv4 address for eth0: 10.19.0.5
Swap usage:0%IPv4 address for eth1: 10.114.0.2
Processes:101
* Introducing Expanded Security Maintenance for Applications.
Receive updates to over 25,000 software packages with your
Ubuntu Pro subscription. Free for personal use.
https://ubuntu.com/pro
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
WARNING: Your password has expired.
You must change your password now and login again!
New password:
Retype new password:
passwd: password updated successfully
Connection to 161.35.71.158 closed.

La contraseña que ingreses aquí será la contraseña regular para el usuario greenlight en tu droplet (es decir, la contraseña que necesitarás escribir siempre que estés conectado y quieras ejecutar un comando sudo). Para ser claro, no es una contraseña SSH para conectarse como el usuario greenlight.

Adelante y escribe la contraseña que desees, confírmala y verifica que veas un mensaje que indique que el cambio de contraseña ha sido exitoso. La conexión SSH será automáticamente terminada.

Si vuelves a conectarte como el usuario greenlight, todo debería funcionar normalmente y deberías poder ejecutar comandos en el droplet. Así:

$ ssh greenlight@161.35.71.158
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-60-generic x86_64)
...
greenlight@greenlight-production:~$ whoami
greenlight

Mientras estamos conectados al droplet, echemos un vistazo rápido y verifiquemos que algunas cosas clave estén funcionando como esperamos. Primero, verifiquemos que el binario de migrate se haya descargado y esté en la ruta del sistema ejecutándolo con la bandera -version. Si todo está bien, esto debería imprimir el número de versión de migrate de la siguiente manera:

greenlight@greenlight-production:~$ migrate -version
4.14.1

A continuación, verifiquemos que PostgreSQL esté en ejecución y que podamos conectarnos a él usando el DSN en la variable de entorno GREENLIGHT_DB_DSN:

greenlight@greenlight-production:~$ psql $GREENLIGHT_DB_DSN
psql (14.6 (Ubuntu 14.6-0ubuntu0.22.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
greenlight=> exit

Finalmente, verifiquemos que Caddy esté en ejecución llamando a systemctl status caddy para ver el estado del servicio de fondo de Caddy. Aquí necesitarás usar sudo para ejecutar este comando como root.

greenlight@greenlight-production:~$ sudo systemctl status caddy
● caddy.service - Caddy
Loaded: loaded (/lib/systemd/system/caddy.service; enabled; vendor preset: enabled)
Active: active (running) since Sun 2021-04-18 15:23:32 EDT; 37min ago
Docs: https://caddyserver.com/docs/
Main PID: 586 (caddy)
Tasks: 7 (limit: 512)
Memory: 37.6M
CGroup: /system.slice/caddy.service
└─586 /usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
...

Deberías ver que el estado del servicio de Caddy es activo (running), confirmando que Caddy está funcionando correctamente. Esto significa que deberías poder visitar tu droplet directamente en un navegador web a través de http://<la_ip_de_tu_droplet>, y deberías ver la siguiente página de bienvenida de Caddy.

20.2 Conectandonos al droplet

Para facilitar un poco la conexión al droplet y para no tener que recordar la dirección IP, agreguemos rápidamente una regla de makefile para inicializar una conexión SSH al droplet como el usuario greenlight. Así:

...
# ==================================================================================== #
# PRODUCTION
# ==================================================================================== #
production_host_ip = &#39;161.35.71.158&#39;
## production/connect: connect to the production server
.PHONY: production/connect
production/connect:
ssh greenlight@${production_host_ip}

Una vez que eso esté configurado, puedes conectarte a tu droplet siempre que lo necesites simplemente escribiendo make production/connect:

$ make production/connect
ssh greenlight@'161.35.71.158'
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-65-generic x86_64)
...
greenlight@greenlight-production:~$

20.2 Informacion adicional

20.2 Futuros cambios en la configuracion droplet

Si necesitas hacer más cambios en la configuración o ajustes de tu droplet, puedes crear un script adicional remote/setup/02.sh y luego ejecutarlo de la siguiente manera:

$ rsync -rP --delete ./remote/setup greenlight@161.35.71.158:~
$ ssh -t greenlight@161.35.71.158 "sudo bash /home/greenlight/setup/02.sh"

20.3 Deploy y ejecucion de migraciones

En este punto, nuestro droplet está configurado con todo el software y las cuentas de usuario que necesitamos, así que pasemos al proceso de implementación y ejecución de nuestra aplicación de API.

A un nivel muy alto, nuestro proceso de implementación consistirá en tres acciones:

  1. Copiar el binario de la aplicación y los archivos de migración de SQL al droplet.
  2. Ejecutar las migraciones contra la base de datos PostgreSQL en el droplet.
  3. Iniciar el binario de la aplicación como un servicio en segundo plano.

Por ahora nos enfocaremos solo en los pasos 1 y 2, y abordaremos la ejecución de nuestra aplicación como un servicio en segundo plano en el próximo capítulo.

Comencemos creando una nueva regla make production/deploy/api en nuestro makefile, que utilizaremos para ejecutar automáticamente estos dos primeros pasos. Así:

...
# ==================================================================================== #
# PRODUCTION
# ==================================================================================== #
production_host_ip = &quot;161.35.71.158&quot;
## production/connect: connect to the production server
.PHONY: production/connect
production/connect:
ssh greenlight@${production_host_ip}
## production/deploy/api: deploy the api to production
.PHONY: production/deploy/api
production/deploy/api:
rsync -P ./bin/linux_amd64/api greenlight@${production_host_ip}:~
rsync -rP --delete ./migrations greenlight@${production_host_ip}:~
ssh -t greenlight@${production_host_ip} &#39;migrate -path ~/migrations -database $$GREENLIGHT_DB_DSN up&#39;

Vamos a desglosar rápidamente lo que hace esta nueva regla.

Primero, utiliza el comando rsync para copiar el binario ejecutable ./bin/linux_amd64/api (el construido específicamente para Linux) y la carpeta ./migrations en el directorio de inicio del usuario greenlight en el droplet.

Luego utiliza el comando ssh con la bandera -t para ejecutar estas migraciones de base de datos en nuestro droplet como el usuario greenlight con el siguiente comando:

'migrate -path ~/migrations -database $$GREENLIGHT_DB_DSN up'

Debido a que el símbolo $ tiene un significado especial en los makefiles, lo estamos escapando en el comando anterior prefijándolo con un caracter $ adicional como $$ . Esto significa que el comando que realmente se ejecuta en nuestro droplet será ‘migrate -path ~/migrations -database $GREENLIGHT_DB_DSN up’, y a su vez, la migración se ejecutará utilizando la variable de entorno del droplet GREENLIGHT_DB_DSN.

También es importante tener en cuenta que estamos rodeando este comando con comillas simples. Si usáramos comillas dobles, sería una cadena interpretada y necesitaríamos usar un caracter de escape adicional \ así:

"migrate -path ~/migrations -database \$$GREENLIGHT_DB_DSN up"

¡Muy bien, vamos a probar esto! Ve adelante y ejecuta la regla make production/deploy/api que acabamos de hacer. Deberías ver que todos los archivos se copian correctamente (esto puede tomar uno o dos minutos para completarse dependiendo de la velocidad de tu conexión), y luego las migraciones se aplican a tu base de datos. Así:

$ make production/deploy/api
rsync -rP --delete ./bin/linux_amd64/api greenlight@"161.35.71.158":~
api
7,618,560 100%
119.34kB/s
0:01:02 (xfr#1, to-chk=13/14)
rsync -rP --delete ./migrations greenlight@"161.35.71.158":~
sending incremental file list
migrations/
migrations/000001_create_movies_table.down.sql
28 100%
27.34kB/s
0:00:00 (xfr#2, to-chk=11/14)
migrations/000001_create_movies_table.up.sql
286 100%
279.30kB/s
0:00:00 (xfr#3, to-chk=10/14)
migrations/000002_add_movies_check_constraints.down.sql
198 100% 193.36kB/s
0:00:00 (xfr#4, to-chk=9/14)
migrations/000002_add_movies_check_constraints.up.sql
289 100%
282.23kB/s
0:00:00 (xfr#5, to-chk=8/14)
migrations/000003_add_movies_indexes.down.sql
78 100%
76.17kB/s
0:00:00 (xfr#6, to-chk=7/14)
migrations/000003_add_movies_indexes.up.sql
170 100%
166.02kB/s
0:00:00 (xfr#7, to-chk=6/14)
migrations/000004_create_users_table.down.sql
27 100%
26.37kB/s
0:00:00 (xfr#8, to-chk=5/14)
migrations/000004_create_users_table.up.sql
294 100%
287.11kB/s
0:00:00 (xfr#9, to-chk=4/14)
migrations/000005_create_tokens_table.down.sql
28 100%
27.34kB/s
0:00:00 (xfr#10, to-chk=3/14)
migrations/000005_create_tokens_table.up.sql
203 100%
99.12kB/s
0:00:00 (xfr#11, to-chk=2/14)
migrations/000006_add_permissions.down.sql
73 100%
35.64kB/s
0:00:00 (xfr#12, to-chk=1/14)
migrations/000006_add_permissions.up.sql
452 100% 220.70kB/s
0:00:00 (xfr#13, to-chk=0/14)
ssh -t greenlight@"161.35.71.158" 'migrate -path ~/migrations -database $GREENLIGHT_DB_DSN up'
1/u create_movies_table (11.782733ms)
2/u add_movies_check_constraints (23.109006ms)
3/u add_movies_indexes (30.61223ms)
4/u create_users_table (39.890662ms)
5/u create_tokens_table (48.659641ms)
6/u add_permissions (58.23243ms)
Connection to 161.35.71.158 closed.

Vamos a conectarnos rápidamente a nuestro droplet y verificar que todo haya funcionado.

$ make production/connect
ssh greenlight@'161.35.71.158'
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-65-generic x86_64)
...
greenlight@greenlight-production:~$ ls -R
.:
api
migrations
./migrations:
000001_create_movies_table.down.sql
000001_create_movies_table.up.sql
000002_add_movies_check_constraints.down.sql
000002_add_movies_check_constraints.up.sql
000003_add_movies_indexes.down.sql
000003_add_movies_indexes.up.sql
000004_create_users_table.down.sql
000004_create_users_table.up.sql
000005_create_tokens_table.down.sql
000005_create_tokens_table.up.sql
000006_add_permissions.down.sql
000006_add_permissions.up.sql

Hasta ahora todo va bien. Podemos ver que el directorio de inicio de nuestro usuario greenlight contiene el binario ejecutable api y una carpeta que contiene los archivos de migración.

También vamos a conectarnos a la base de datos usando psql y verificar que las tablas hayan sido creadas usando el comando meta \dt. Tu salida debería lucir similar a esto:

greenlight@greenlight-production:~$ psql $GREENLIGHT_DB_DSN
psql (12.6 (Ubuntu 12.6-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
greenlight=> \dt
List of relations
Schema |
Name
| Type
|
Owner
--------+-------------------+-------+------------
public | movies
public | permissions
| table | greenlight
| table | greenlight
public | schema_migrations | table | greenlight
public | tokens| table | greenlight
public | users| table | greenlight
public | users_permissions | table | greenlight
(6 rows)

20.3 Corriendo la API

Mientras estamos conectados al droplet, intentemos ejecutar el binario ejecutable api. No podemos escuchar conexiones entrantes en el puerto 80 porque Caddy ya lo está utilizando, así que escucharemos en el puerto sin restricciones 4000 en su lugar.

Si has estado siguiendo, el puerto 4000 en tu droplet actualmente debería estar bloqueado por las reglas del firewall, así que necesitaremos relajar esto temporalmente para permitir solicitudes entrantes. Adelante y hazlo de la siguiente manera:

Mientras estamos conectados al droplet, intentemos ejecutar el binario ejecutable api. No podemos escuchar conexiones entrantes en el puerto 80 porque Caddy ya lo está utilizando, así que escucharemos en el puerto sin restricciones 4000 en su lugar.

Si has estado siguiendo, el puerto 4000 en tu droplet actualmente debería estar bloqueado por las reglas del firewall, así que necesitaremos relajar esto temporalmente para permitir solicitudes entrantes. Adelante y hazlo de la siguiente manera:

greenlight@greenlight-production:~$ sudo ufw allow 4000/tcp
Rule added
Rule added (v6)

Y luego inicia la API con el siguiente comando:

greenlight@greenlight-production:~$ ./api -port=4000 -db-dsn=$GREENLIGHT_DB_DSN -env=production
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

En este punto, deberías poder visitar http://<la_ip_de_tu_droplet>:4000/v1/healthcheck en tu navegador web y obtener una respuesta exitosa del punto final de verificación de salud.

Por último, vuelve a tu ventana de terminal SSH y presiona Ctrl+C para detener la ejecución de la API en tu droplet. Deberías ver que se apaga con gracia de la siguiente manera:

greenlight@greenlight-production:~$ /home/greenlight/api -port=4000 -db-dsn=$GREENLIGHT_DB_DSN -env=production
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development
^Ctime=2023-09-10T10:59:14.722+02:00 level=INFO msg="shutting down server" signal=terminated
time=2023-09-10T10:59:14.722+02:00 level=INFO msg="completing background tasks" addr=:4000
time=2023-09-10T10:59:18.722+02:00 level=INFO msg="stopped server" addr=:4000

20.4 Corriendo la API como un background service

Ahora que sabemos que nuestro ejecutable de API funciona bien en nuestro droplet de producción, el siguiente paso es configurarlo para que se ejecute como un servicio en segundo plano, incluyendo su inicio automático cuando el droplet se reinicie.

Existen algunas herramientas diferentes que podríamos usar para hacer esto, pero en este libro utilizaremos systemd, una colección de herramientas para gestionar servicios que se incluye con Ubuntu (y muchas otras distribuciones de Linux).

Para ejecutar nuestra aplicación de API como un servicio en segundo plano, lo primero que necesitamos hacer es crear un archivo de unidad, que informa a systemd cómo y cuándo ejecutar el servicio.

Si estás siguiendo, regresa a una ventana de terminal en tu máquina local y crea un nuevo archivo remote/production/api.service en el directorio de tu proyecto:

$ mkdir remote/production
$ touch remote/production/api.service

Y luego agrega el siguiente marcado:

[Unit]
# Description is a human-readable name for the service.
Description=Greenlight API service
# Wait until PostgreSQL is running and the network is &quot;up&quot; before starting the service.
After=postgresql.service
After=network-online.target
Wants=network-online.target
# Configure service start rate limiting. If the service is (re)started more than 5 times
# in 600 seconds then don&#39;t permit it to start anymore.
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
# Execute the API binary as the greenlight user, loading the environment variables from
# /etc/environment and using the working directory /home/greenlight.
Type=exec
User=greenlight
Group=greenlight
EnvironmentFile=/etc/environment
WorkingDirectory=/home/greenlight
ExecStart=/home/greenlight/api -port=4000 -db-dsn=${GREENLIGHT_DB_DSN} -env=production
# Automatically restart the service after a 5-second wait if it exits with a non-zero
# exit code. If it restarts more than 5 times in 600 seconds, then the rate limit we
# configured above will be hit and it won&#39;t be restarted anymore.
Restart=on-failure
RestartSec=5
[Install]
# Start the service automatically at boot time (the &#39;multi-user.target&#39; describes a boot
# state when the system will accept logins).
WantedBy=multi-user.target

Ahora que tenemos un archivo de unidad configurado, el siguiente paso es instalar este archivo de unidad en nuestro droplet y iniciar el servicio. Básicamente, necesitamos hacer tres cosas:

  1. Para instalar el archivo de unidad, necesitamos copiarlo en la carpeta /etc/systemd/system/ en nuestro droplet. Como esta carpeta pertenece al usuario root en nuestro droplet, necesitamos dividir esto en dos pasos: primero necesitamos copiar el archivo de unidad en el directorio de inicio del usuario greenlight, y en segundo lugar, usar el comando sudo mv para moverlo a su ubicación final.

  2. Luego necesitamos ejecutar el comando systemctl enable api en nuestro droplet para que systemd sea consciente del nuevo archivo de unidad y habilite automáticamente el servicio cuando el droplet se reinicie.

  3. Finalmente, necesitamos ejecutar systemctl restart api para iniciar (o reiniciar) el servicio.

Todos estos tareas deberán ser ejecutadas con sudo.

Actualicemos la regla makefile production/deploy/api para automatizar estas tareas para nosotros. Así:

...
# ==================================================================================== #
# PRODUCTION
# ==================================================================================== #
production_host_ip = &#39;161.35.71.158&#39;
...
## production/deploy/api: deploy the api to production
.PHONY: production/deploy/api
production/deploy/api:
rsync -P ./bin/linux_amd64/api greenlight@${production_host_ip}:~
rsync -rP --delete ./migrations greenlight@${production_host_ip}:~
rsync -P ./remote/production/api.service greenlight@${production_host_ip}:~
ssh -t greenlight@${production_host_ip} &#39;\
migrate -path ~/migrations -database $$GREENLIGHT_DB_DSN up \
&amp;&amp; sudo mv ~/api.service /etc/systemd/system/ \
&amp;&amp; sudo systemctl enable api \
&amp;&amp; sudo systemctl restart api \
&#39;

Guarda el archivo makefile y luego procede a ejecutar nuevamente la regla production/deploy/api. Se te pedirá ingresar la contraseña del usuario greenlight mientras se ejecuta, y la regla debería completarse sin errores. La salida que verás debería lucir similar a esto:

$ make production/deploy/api
rsync -P ./bin/linux_amd64/api greenlight@&quot;161.35.71.158&quot;:~
api
7,700,480 100%
2.39GB/s
0:00:00 (xfr#1, to-chk=0/1)
rsync -rP --delete ./migrations greenlight@&quot;161.35.71.158&quot;:~
sending incremental file list
migrations/000001_create_movies_table.down.sql
28 100%
0.00kB/s
0:00:00 (xfr#1, to-chk=11/13)
migrations/000001_create_movies_table.up.sql
286 100% 279.30kB/s
0:00:00 (xfr#2, to-chk=10/13)
migrations/000002_add_movies_check_constraints.down.sql
198 100%
193.36kB/s
0:00:00 (xfr#3, to-chk=9/13)
migrations/000002_add_movies_check_constraints.up.sql
289 100%
282.23kB/s
0:00:00 (xfr#4, to-chk=8/13)
migrations/000003_add_movies_indexes.down.sql
78 100%
76.17kB/s
0:00:00 (xfr#5, to-chk=7/13)
migrations/000003_add_movies_indexes.up.sql
170 100%
166.02kB/s
0:00:00 (xfr#6, to-chk=6/13)
migrations/000004_create_users_table.down.sql
27 100%
26.37kB/s
0:00:00 (xfr#7, to-chk=5/13)
migrations/000004_create_users_table.up.sql
294 100%
287.11kB/s
0:00:00 (xfr#8, to-chk=4/13)
migrations/000005_create_tokens_table.down.sql
28 100%
27.34kB/s
0:00:00 (xfr#9, to-chk=3/13)
migrations/000005_create_tokens_table.up.sql
203 100%
198.24kB/s
0:00:00 (xfr#10, to-chk=2/13)
migrations/000006_add_permissions.down.sql
73 100%
71.29kB/s
0:00:00 (xfr#11, to-chk=1/13)
migrations/000006_add_permissions.up.sql
452 100%
441.41kB/s
0:00:00 (xfr#12, to-chk=0/13)
rsync -P ./remote/production/api.service greenlight@&quot;161.35.71.158&quot;:~
api.service
1,266 100%
0.00kB/s
0:00:00 (xfr#1, to-chk=0/1)
ssh -t greenlight@&quot;161.35.71.158&quot; &#39;\
migrate -path ~/migrations -database $GREENLIGHT_DB_DSN up \
&amp;&amp; sudo mv ~/api.service /etc/systemd/system/ \
&amp;&amp; sudo systemctl enable api \
&amp;&amp; sudo systemctl restart api \
&#39;
no change
[sudo] password for greenlight:
Created symlink /etc/systemd/system/multi-user.target.wants/api.service → /etc/systemd/system/api.service.
Connection to 161.35.71.158 closed.

A continuación, conectémonos al droplet y verifiquemos el estado de nuestro nuevo servicio de API utilizando el comando sudo systemctl status api:

$ make production/connect
greenlight@greenlight-production:~$ sudo systemctl status api
● api.service - Greenlight API service
Loaded: loaded (/etc/systemd/system/api.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2023-02-23 17:28:18 CET; 1min 7s ago
Main PID: 2717 (api)
Tasks: 5 (limit: 512)
Memory: 2.9M
CPU: 10ms
CGroup: /system.slice/api.service
└─2717 /home/greenlight/api -port=4000 -db-dsn=postgres://greenlight:pa55word1234@localhost/greenlight -env=production
Feb 23 17:28:18 greenlight-production systemd[1]: Starting Greenlight API service...
Feb 23 17:28:18 greenlight-production systemd[1]: Started Greenlight API service.
Feb 23 17:28:18 greenlight-production api[2717]: time=2023-02-23T17:28:18.722+02:00 level=INFO msg=&quot;database connection pool established&quot;
Feb 23 17:28:18 greenlight-production api[2717]: time=2023-02-23T17:28:18.722+02:00 level=INFO msg=&quot;starting server&quot; ...

¡Genial! Esto confirma que nuestro servicio de API se está ejecutando correctamente en segundo plano y, en mi caso, tiene el PID (ID de proceso) 2717.

Por interés, también vamos a listar rápidamente los procesos en ejecución para nuestro usuario greenlight:

greenlight@greenlight-production:~$ ps -U greenlight
PID TTYTIME CMD
2717 ?00:00:00 api
2749 ?00:00:00 systemd
2750 ?00:00:00 (sd-pam)
2807 ?00:00:00 sshd
2808 pts/000:00:00 bash
2859 pts/000:00:00 ps

Eso cuadra: podemos ver el proceso api listado aquí con el PID 2717, confirmando que es realmente nuestro usuario greenlight quien está ejecutando el binario api.

Por último, vamos a verificar desde una perspectiva externa e intentemos hacer una solicitud a nuestro punto final de verificación de salud nuevamente, ya sea en un navegador o con curl. Deberías obtener una respuesta exitosa como esta:

$ curl 161.35.71.158:4000/v1/healthcheck
{
  "status": "available",
  "system_info": {
    "environment": "production",
    "version": "1c9b6ff48ea800acdf4f5c6f5c3b62b98baf2bd7"
  }
}

20.4 Reseteando después del reinicio

Al ejecutar el comando systemctl enable api en nuestro makefile después de haber copiado el archivo de unidad systemd, nuestro servicio de API debería iniciarse automáticamente después de que el droplet se reinicie.

Si lo deseas, puedes verificar esto por ti mismo. Mientras estás conectado al droplet a través de SSH, procede a reiniciarlo (asegúrate de estar conectado al droplet y no reiniciar accidentalmente tu máquina local).

Espera un momento para que se complete el reinicio y el droplet vuelva a estar en línea. Luego, podrás volver a conectarte y utilizar systemctl status para verificar que el servicio esté en funcionamiento nuevamente. Así:

$ make production/connect
greenlight@greenlight-production:~$ sudo systemctl status api.service
[sudo] password for greenlight:
● api.service - Greenlight API service
Loaded: loaded (/etc/systemd/system/api.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2023-02-23 17:34:55 CET; 1min 33s ago
Main PID: 895 (api)
Tasks: 5 (limit: 512)
Memory: 8.8M
CPU: 15ms
CGroup: /system.slice/api.service
└─895 /home/greenlight/api -port=4000 -db-dsn=postgres://greenlight:pa55word1234@localhost/greenlight -env=production
Feb 23 17:28:18 greenlight-production systemd[1]: Starting Greenlight API service...
Feb 23 17:28:18 greenlight-production systemd[1]: Started Greenlight API service.
Feb 23 17:28:18 greenlight-production api[2717]: time=2023-02-23T17:28:18.722+02:00 level=INFO msg="database connection pool established"
Feb 23 17:28:18 greenlight-production api[2717]: time=2023-02-23T17:28:18.722+02:00 level=INFO msg="starting server" ...

20.4 Deshabilitando el puerto 4000

Si has estado siguiendo los pasos de esta sección del libro, vamos a revertir el cambio temporal en el firewall que hicimos anteriormente y a no permitir el tráfico en el puerto 4000 de nuevo.

greenlight@greenlight-production:~$ sudo ufw delete allow 4000/tcp
Rule deleted
Rule deleted (v6)
greenlight@greenlight-production:~$ sudo ufw status
Status: active
ToActionFrom
------------
22ALLOWAnywhere
80/tcpALLOWAnywhere
443/tcpALLOWAnywhere
22 (v6)ALLOWAnywhere (v6)
80/tcp (v6)ALLOWAnywhere (v6)
443/tcp (v6)ALLOWAnywhere (v6)

20.4 Información adicional

20.4 Escuchando en un puerto restringido

Si no estás planeando ejecutar tu aplicación detrás de un proxy inverso y deseas escuchar las solicitudes directamente en los puertos 80 o 443, necesitarás configurar tu archivo de unidad para que el servicio tenga la capacidad CAP_NET_BIND_SERVICE (lo que le permitirá enlazarse a un puerto restringido). Por ejemplo:

[Unit]
Description=Greenlight API service
After=postgresql.service
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
Type=exec
User=greenlight
Group=greenlight
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
EnvironmentFile=/etc/environment
WorkingDirectory=/home/greenlight
ExecStart=/home/greenlight/api -port=80 -db-dsn=${GREENLIGHT_DB_DSN} -env=production
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
20.4 Viendo logs

Es posible ver los registros de tu servicio en segundo plano utilizando el comando journalctl, de la siguiente manera:

$ sudo journalctl -u api
-- Logs begin at Sun 2021-04-18 14:55:45 EDT, end at Mon 2021-04-19 03:39:55 EDT. --
Apr 19 03:24:35 greenlight-production systemd[1]: Starting Greenlight API service...
Apr 19 03:24:35 greenlight-production systemd[1]: Started Greenlight API service.
Apr 19 03:24:35 greenlight-production api[6997]: time=2023-02-23T17:28:18.722+02:00 level=INFO msg=&quot;database connection pool established&quot;
Apr 19 03:24:35 greenlight-production api[6997]: time=2023-02-23T17:28:18.722+02:00 level=INFO msg=&quot;starting server&quot; ...

El comando journalctl es realmente poderoso y ofrece una amplia variedad de parámetros que puedes utilizar para filtrar tus mensajes de registro y personalizar el formato. Este artículo proporciona una excelente introducción a los detalles de journalctl y vale la pena leerlo.

20.4 Congiurando proveedor SMTP

Nuestro archivo de unidad está configurado actualmente para iniciar nuestra API con el siguiente comando:

ExecStart=/home/greenlight/api -port=4000 -db-dsn=${GREENLIGHT_DB_DSN} -env=production

Es importante recordar que, aparte de los flags port, db-dsn y env que estamos especificando aquí, nuestra aplicación seguirá utilizando los valores predeterminados para los otros ajustes que están codificados en el archivo cmd/api/main.go, incluidas las credenciales SMTP para tu bandeja de entrada de Mailtrap. En circunstancias normales, querrías establecer tus credenciales SMTP de producción como parte de este comando en el archivo de unidad también.

20.4 Opciones adicionales del archivo de unidad

Los archivos de unidad de systemd ofrecen una amplia gama de opciones de configuración, y apenas hemos rozado la superficie en este capítulo. Para obtener más información, el artículo Understanding Systemd Units and Unit Files es una excelente visión general, y puedes encontrar información completa (aunque densa) en las páginas del manual.

También hay este gist que describe las diversas opciones de seguridad que puedes utilizar para fortalecer tu servicio.

20.5 Usando Caddy como reverse proxy

Ahora estamos en la posición en la que tenemos nuestra aplicación Greenlight API ejecutándose como un servicio en segundo plano en nuestro droplet, y escuchando solicitudes HTTP en el puerto 4000. Y también tenemos Caddy ejecutándose como un servicio en segundo plano y escuchando solicitudes HTTP en el puerto 80.

Por lo tanto, el siguiente paso en la configuración de nuestro entorno de producción es configurar Caddy para actuar como un proxy inverso y reenviar cualquier solicitud HTTP que reciba a nuestra API.

La forma más sencilla de configurar Caddy es crear un archivo Caddyfile, que contiene una serie de reglas que describen lo que queremos que haga Caddy. Si estás siguiendo, por favor crea un nuevo archivo remote/production/Caddyfile en el directorio de tu proyecto:

$ touch remote/production/Caddyfile

Y luego agrega el siguiente contenido, asegurándote de reemplazar la dirección IP con la dirección de tu propio droplet:

http://161.35.71.158 {
reverse_proxy localhost:4000
}

Como probablemente puedas deducir al mirarlo, esta regla le indica a Caddy que queremos que escuche las solicitudes HTTP en 161.35.71.158 y luego actúe como un proxy inverso, enviando la solicitud al puerto 4000 en la máquina local (donde está escuchando nuestra API).

Consejo: Los Caddyfiles tienen una gran cantidad de configuraciones disponibles y demostraremos algunas de las más comunes a medida que avancemos en este capítulo. Pero si planeas utilizar Caddy en tus propios proyectos de producción, te recomiendo que leas la documentación oficial de Caddyfile, que es excelente.

Para gestionar esto, actualicemos nuestra regla production/deploy/api. Esto seguirá el mismo patrón básico que utilizamos en el capítulo anterior para cargar nuestro archivo api.service, pero en lugar de eso, copiaremos nuestro Caddyfile en /etc/caddy/Caddyfile en el servidor.

# ==================================================================================== #
# PRODUCTION
# ==================================================================================== #
production_host_ip = &#39;161.35.71.158&#39;
...
## production/deploy/api: deploy the api to production
.PHONY: production/deploy/api
production/deploy/api:
rsync -P ./bin/linux_amd64/api greenlight@${production_host_ip}:~
rsync -rP --delete ./migrations greenlight@${production_host_ip}:~
rsync -P ./remote/production/api.service greenlight@${production_host_ip}:~
rsync -P ./remote/production/Caddyfile greenlight@${production_host_ip}:~
ssh -t greenlight@${production_host_ip} &#39;\
migrate -path ~/migrations -database $$GREENLIGHT_DB_DSN up \
&amp;&amp; sudo mv ~/api.service /etc/systemd/system/ \
&amp;&amp; sudo systemctl enable api \
&amp;&amp; sudo systemctl restart api \
&amp;&amp; sudo mv ~/Caddyfile /etc/caddy/ \
&amp;&amp; sudo systemctl reload caddy \
&#39;

Una vez que hayas realizado ese cambio, procede a ejecutar la regla para implementar el archivo Caddyfile en producción:

$ make production/deploy/api
rsync -P ./bin/linux_amd64/api greenlight@&quot;161.35.71.158&quot;:~
api
7,634,944 100%
70.67MB/s
0:00:00 (xfr#1, to-chk=0/1)
rsync -rP --delete ./migrations greenlight@&quot;161.35.71.158&quot;:~
sending incremental file list
migrations/000001_create_movies_table.down.sql
28 100%
0.00kB/s
0:00:00 (xfr#1, to-chk=11/13)
migrations/000001_create_movies_table.up.sql
286 100%
279.30kB/s
0:00:00 (xfr#2, to-chk=10/13)
migrations/000002_add_movies_check_constraints.down.sql
198 100%
193.36kB/s
0:00:00 (xfr#3, to-chk=9/13)
migrations/000002_add_movies_check_constraints.up.sql
289 100%
282.23kB/s
0:00:00 (xfr#4, to-chk=8/13)
migrations/000003_add_movies_indexes.down.sql
78 100%
76.17kB/s
0:00:00 (xfr#5, to-chk=7/13)
migrations/000003_add_movies_indexes.up.sql
170 100%
166.02kB/s
0:00:00 (xfr#6, to-chk=6/13)
migrations/000004_create_users_table.down.sql
27 100%
26.37kB/s
0:00:00 (xfr#7, to-chk=5/13)
migrations/000004_create_users_table.up.sql
294 100%
287.11kB/s
0:00:00 (xfr#8, to-chk=4/13)
migrations/000005_create_tokens_table.down.sql
28 100%
27.34kB/s
0:00:00 (xfr#9, to-chk=3/13)
migrations/000005_create_tokens_table.up.sql
203 100% 198.24kB/s
0:00:00 (xfr#10, to-chk=2/13)
migrations/000006_add_permissions.down.sql
73 100%
71.29kB/s
0:00:00 (xfr#11, to-chk=1/13)
migrations/000006_add_permissions.up.sql
452 100%
441.41kB/s
0:00:00 (xfr#12, to-chk=0/13)
rsync -P ./remote/production/api.service greenlight@&quot;161.35.71.158&quot;:~
api.service
1,266 100%
0.00kB/s
0:00:00 (xfr#1, to-chk=0/1)
rsync -P ./remote/production/Caddyfile greenlight@&quot;161.35.71.158&quot;:~
Caddyfile
293 100%
0.00kB/s
0:00:00 (xfr#1, to-chk=0/1)
ssh -t greenlight@&quot;161.35.71.158&quot; &#39;\
migrate -path ~/migrations -database $GREENLIGHT_DB_DSN up \
&amp;&amp; sudo mv ~/api.service /etc/systemd/system/ \
&amp;&amp; sudo systemctl enable api \
&amp;&amp; sudo systemctl restart api \
&amp;&amp; sudo mv ~/Caddyfile /etc/caddy/ \
&amp;&amp; sudo systemctl reload caddy \
&#39;
no change
[sudo] password for greenlight:
Connection to 161.35.71.158 closed.

Deberías ver que el archivo Caddyfile se copia correctamente y la recarga se ejecuta sin problemas ni errores.

En este punto, puedes visitar http://<your_droplet_ip>/v1/healthcheck en un navegador web y deberías encontrar que la solicitud se reenvía correctamente desde Caddy a nuestra API.

20.5 Bloqueando el acceso a las métricas de la aplicación

Al estar en el navegador, naveguemos hasta el punto final GET /debug/vars que muestra nuestras métricas de la aplicación.

Como mencionamos anteriormente, no es una buena idea que esta información sensible sea accesible públicamente.

Afortunadamente, es muy fácil bloquear el acceso a esto agregando una nueva directiva respond a nuestro archivo Caddyfile de la siguiente manera:

http://161.35.71.158 {
respond /debug/* &quot;Not Permitted&quot; 403
reverse_proxy localhost:4000
}

Con esta nueva directiva, le estamos indicando a Caddy que envíe una respuesta 403 Forbidden para todas las solicitudes que tengan una ruta de URL que comience con /debug/.

Continúa y despliega este cambio en producción nuevamente, y cuando actualices la página en tu navegador web, deberías encontrar que ahora está bloqueada.

$ make production/deploy/api

Aunque las métricas ya no son accesibles públicamente, aún puedes acceder a ellas conectándote a tu droplet a través de SSH y haciendo una solicitud a http://localhost:4000/debug/vars.

$ make production/connect
greenlight@greenlight-production:~$ curl http://localhost:4000/debug/vars
{
&quot;cmdline&quot;: ...,
&quot;database&quot;: ...,
&quot;goroutines&quot;: 7,
&quot;memstats&quot;: ...,
&quot;timestamp&quot;: 1618820037,
&quot;total_processing_time_μs&quot;: 1305,
&quot;total_requests_received&quot;: 8,
&quot;total_responses_sent&quot;: 7,
&quot;total_responses_sent_by_status&quot;: {&quot;200&quot;: 3, &quot;404&quot;: 4},
&quot;version&quot;: &quot;v1.0.0-1-gf27fd0f&quot;
}

O alternativamente, puedes abrir un túnel SSH al droplet y verlas utilizando un navegador web en tu máquina local. Por ejemplo, podrías abrir un túnel SSH entre el puerto 4000 en el droplet y el puerto 9999 en tu máquina local ejecutando el siguiente comando (asegúrate de reemplazar ambas direcciones IP con la de tu propio droplet):

$ ssh -L :9999:161.35.71.158:4000 greenlight@161.35.71.158

Mientras ese túnel esté activo, deberías poder visitar http://localhost:9999/debug/vars en tu navegador web y ver las métricas de tu aplicación:

20.5 Usando domain name

Para el próximo paso de nuestro despliegue, vamos a configurar Caddy para que podamos acceder a nuestro droplet a través de un nombre de dominio, en lugar de necesitar usar la dirección IP.

Si deseas seguir este paso, necesitarás acceso a un nombre de dominio y la capacidad de actualizar los registros DNS para ese nombre de dominio. Si no tienes un nombre de dominio disponible que puedas usar, puedes obtener uno gratis a través del servicio Freenom.

Voy a usar el dominio greenlight.nahueldev23.net en el código de ejemplo aquí, pero deberías cambiar esto por tu propio dominio si estás siguiendo el ejemplo.

Lo primero que deberás hacer es configurar los registros DNS para tu nombre de dominio de manera que contengan un registro A que apunte a la dirección IP de tu droplet. Así que en mi caso, el registro DNS se vería así:

A greenlight.alexedwards.net 161.35.71.158

Nota: Si no estás seguro de cómo modificar tus registros DNS, tu registrador de nombres de dominio debería proporcionar orientación y documentación.

Una vez que hayas configurado el registro DNS, la siguiente tarea es actualizar el archivo Caddyfile para usar tu nombre de dominio en lugar de la dirección IP de tu droplet. Hazlo de la siguiente manera (recuerda reemplazar greenlight.nahueldev23.net con tu propio nombre de dominio):

http://greenlight.nahueldever.net {
  respond /debug/* &quot;Not Permitted&quot; 403
  reverse_proxy localhost:4000
}

Y luego vuelve a implementar el archivo Caddyfile en tu droplet:

$ make production/deploy/api

Una vez hecho eso, ahora deberías poder acceder a la API a través de tu nombre de dominio visitando http://<tu_nombre_de_dominio>/v1/healthcheck en tu navegador:

20.5 Habilitando HTTPS

Ahora que tenemos un nombre de dominio configurado, podemos aprovechar una de las características principales de Caddy: HTTPS automático.

Caddy manejará automáticamente la provisión y renovación de certificados TLS para tu dominio a través de Let’s Encrypt o ZeroSSL (según disponibilidad), además de redirigir todas las solicitudes HTTP a HTTPS. Es fácil de configurar, muy robusto y te ahorra la carga de tener que hacer un seguimiento de las renovaciones de certificados manualmente.

Para habilitar esto, solo necesitamos actualizar nuestro Caddyfile para que se vea así:

# Set the email address that should be used to contact you if there is a problem with
# your TLS certificates.
{
  email you@example.com
}
# Remove the http:// prefix from your site address.
greenlight.nahueldev23.net {
  respond /debug/* &quot;Not Permitted&quot; 403
  reverse_proxy localhost:4000
}

Una vez más, despliega esta actualización del archivo Caddyfile en tu droplet…

$ make production/deploy/api

Y luego, cuando actualices la página en tu navegador web, deberías encontrar que se redirige automáticamente a una versión HTTPS de la página.

Importante: Si obtienes un error al actualizar la página, espera 30 segundos y luego vuelve a intentarlo. A veces, Caddy puede tardar un momento en completar la configuración del certificado por primera vez.

Si estás utilizando Firefox, también puedes ver la información de la página en tu navegador presionando Ctrl+i.

Podemos ver que la conexión ha sido exitosamente encriptada usando TLS 1.3 y utilizando el conjunto de cifrado TLS_AES_128_GCM_SHA256. Por último, si deseas, puedes usar curl para intentar hacer una solicitud HTTP a la aplicación. Deberías ver que esto emite un 308 Redirección Permanente a la versión HTTPS de la aplicación, como se muestra a continuación:

$ curl -i http://greenlight.nahueldev23.net
HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://greenlight.nahueldev23.net/
Server: Caddy
Date: Mon, 19 Apr 2021 08:36:20 GMT
Content-Length: 0

20.5 información adicional

20.5 Escalando la infraestructura

Antes de lanzar un nuevo servicio, suele ser útil hacer un experimento mental y preguntarse: ¿Qué sucede a medida que aumenta el tráfico al servicio? ¿Cómo lo gestionaría? Para este proyecto, hay un camino claro que podríamos seguir para escalar y adaptar la infraestructura para acomodar el crecimiento. A grandes rasgos, el camino se ve algo así:

  1. Utilizar un solo droplet de baja potencia ejecutando Caddy, PostgreSQL y la aplicación Go. Esto es lo que tenemos actualmente.
  2. Actualizar el droplet para tener más CPU y/o memoria según sea necesario.
  3. Mover PostgreSQL a un droplet separado o usar una base de datos gestionada.
  4. Actualizar droplets/bases de datos gestionadas para tener más CPU y/o memoria según sea necesario.

Si el droplet que ejecuta la aplicación Go es un cuello de botella:

  1. Perfil y optimizar el código de la aplicación Go.
  2. Ejecutar la aplicación Go en varios droplets, detrás de un balanceador de carga.

Si la base de datos PostgreSQL es un cuello de botella:

  1. Perfilar y optimizar la configuración de la base de datos y las consultas de la base de datos.
  2. Si es apropiado, cachear los resultados de las consultas de base de datos costosas/frecuentes.
  3. Si es apropiado, mover algunas operaciones a una base de datos en memoria como Redis.
  4. Comenzar a usar réplicas de base de datos de solo lectura para consultas cuando sea posible.
  5. Comenzar a fragmentar la base de datos.

En otros proyectos, estos pasos y el orden de los mismos probablemente serán diferentes. Y en algunos casos, puede ser sensato pasar directamente a una arquitectura más compleja de inmediato, basándose en los requisitos del negocio, estimaciones de tráfico y resultados de pruebas de carga. Pero tomarse un breve momento para hacer un experimento mental como este es bueno. Si puedes ver un camino claro, proporciona cierta tranquilidad positiva de que el crecimiento puede ser acomodado sin que necesites optimizar prematuramente la infraestructura o la aplicación. Mientras que si no puedes ver un camino claro para acomodar el crecimiento, eso resalta un problema potencial y puede ser sensato revisar tus elecciones de infraestructura.

Post Relacionados