En esta serie de dos posts voy a explicar desde cero como he desplegado este proyecto en AWS. La arquitectura no tiene demasiado misterio, me he decidido por una arquitectura IaaS con una única instancia EC2 que contenga tanto la capa de aplicación como la base de datos. La razón es sencilla. Es lo más eficiente en coste para un proyecto de este tipo smiley

1. Conexión a EC2 e instalación de aplicaciones necesarias

Primero vamos a conectar a la instancia EC2 utilizando para ello la clave privada generada al levantar la instancia:

ssh -i key_pair.pem admin@ec2-54-172-3-252.compute-1.amazonaws.com
admin@ip-172-31-88-32:~$

Antes de nada, lanzamos un apt-get update para actualizar los repositorios:

admin@ip-172-31-88-32:~$ sudo apt-get update
Get:1 http://security.debian.org/debian-security bullseye-security InRelease [48.4 kB]
Get:2 http://cdn-aws.deb.debian.org/debian bullseye InRelease [116 kB]     
Get:3 http://cdn-aws.deb.debian.org/debian bullseye-updates InRelease [44.1 kB]
Get:4 http://cdn-aws.deb.debian.org/debian bullseye-backports InRelease [49.0 kB]
Get:5 http://security.debian.org/debian-security bullseye-security/main Sources [173 kB]
Get:6 http://security.debian.org/debian-security bullseye-security/main amd64 Packages [209 kB]
Get:7 http://security.debian.org/debian-security bullseye-security/main Translation-en [135 kB]
Get:8 http://cdn-aws.deb.debian.org/debian bullseye/main Sources [8633 kB]
Get:9 http://cdn-aws.deb.debian.org/debian bullseye/main amd64 Packages [8184 kB]
Get:10 http://cdn-aws.deb.debian.org/debian bullseye/main Translation-en [6239 kB]
Get:11 http://cdn-aws.deb.debian.org/debian bullseye-updates/main Sources [4812 B]
Get:12 http://cdn-aws.deb.debian.org/debian bullseye-updates/main amd64 Packages [14.6 kB]
Get:13 http://cdn-aws.deb.debian.org/debian bullseye-updates/main Translation-en [7929 B]
Get:14 http://cdn-aws.deb.debian.org/debian bullseye-backports/main Sources [365 kB]
Get:15 http://cdn-aws.deb.debian.org/debian bullseye-backports/main amd64 Packages [367 kB]
Get:16 http://cdn-aws.deb.debian.org/debian bullseye-backports/main Translation-en [301 kB]
Fetched 24.9 MB in 4s (6167 kB/s)                          
Reading package lists... Done

Ahora sí, instalamos el software necesario para poder levantar nuestro site. Se trata de un proyecto Django con BBDD Postgre, por lo que instalaremos lo estrictamente necesario para empezar:

  • Python3
  • Pip
  • Postgresql
  • Virtualenv

Y adicionalmente vamos a instalar Git para ayudarnos a clonar el repositorio desde Github que será el que levantemos en la instancia.

De momento nos vamos a centrar únicamente en instalar lo básico para poder levantar el proyecto Django. Luego ya profundizaremos para montar nginx, etc.

admin@ip-172-31-88-32:~$ sudo apt-get install git virtualenv postgresql python3-pip

Tras las instalaciones chequeamos las versiones instaladas de Python, pip, virtualenv y postgresql:

admin@ip-172-31-88-32:~$ python3 --version
Python 3.9.2
admin@ip-172-31-88-32:~$ pip --version
pip 20.3.4 from /usr/lib/python3/dist-packages/pip (python 3.9)
admin@ip-172-31-88-32:~$ virtualenv --version
virtualenv 20.4.0+ds from /usr/lib/python3/dist-packages/virtualenv/__init__.py

Tras tartar de conectar postgre se recibirán un error de localización no definida. Similar a esto:

admin@ip-172-31-88-32:~$ sudo -u postgres psql
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
                LANGUAGE = (unset),
                LC_ALL = (unset),
                LC_ADDRESS = "es_ES.UTF-8",
                LC_NAME = "es_ES.UTF-8",
                LC_MONETARY = "es_ES.UTF-8",
                LC_PAPER = "es_ES.UTF-8",
                LC_IDENTIFICATION = "es_ES.UTF-8",
                LC_TELEPHONE = "es_ES.UTF-8",
                LC_MEASUREMENT = "es_ES.UTF-8",
                LC_NUMERIC = "es_ES.UTF-8",
                LANG = "C.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to a fallback locale ("C.UTF-8").
psql (13.8 (Debian 13.8-0+deb11u1))
Type "help" for help.

Para solucionarlo vamos a generar los locales que requiera nuestra aplicación. En mi caso es-ES.UTF-8:

admin@ip-172-31-88-32:~$ sudo dpkg-reconfigure locales
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
                LANGUAGE = (unset),
                LC_ALL = "es_ES.UTF-8",
                LC_ADDRESS = "es_ES.UTF-8",
                LC_NAME = "es_ES.UTF-8",
                LC_MONETARY = "es_ES.UTF-8",
                LC_PAPER = "es_ES.UTF-8",
                LC_IDENTIFICATION = "es_ES.UTF-8",
                LC_TELEPHONE = "es_ES.UTF-8",
                LC_MEASUREMENT = "es_ES.UTF-8",
                LC_CTYPE = "es_ES.UTF-8",
                LC_NUMERIC = "es_ES.UTF-8",
                LANG = "C.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to a fallback locale ("C.UTF-8").
Generating locales (this might take a while)...
  en_US.UTF-8... done
  es_ES.UTF-8... done
Generation complete.

Ahora ya si deberíamos poder conectar a postgre, y vemos que tenemos instalada la versión 13.8:

admin@ip-172-31-88-32:~$ sudo -u postgres psql
psql (13.8 (Debian 13.8-0+deb11u1))
Digite «help» para obtener ayuda.
postgres=#

Estamos listos para empezar la configuración del entorno

2. Creando del entorno virtual con virtualenv

De cara a tener un entorno aislado y con las dependencias requeridas para levantar nuestro site Django, vamos a utilizar virtualenv. Es decir, NO vamos a instalar las dependencias cross-instancia, porque eso haría que si a futuro queremos utilizar esta instancia para levantar otro tipo de proyectos en paralelo, pudiésemos tener problemas de dependencias.

Para ello debemos indicar a virtualenv que versión de Python queremos utilizar. Anteriormente vimos que estamos utilizando la versión 3.9.2. Pero veamos cual es el interprete real y donde se ubica en nuestra máquina:

admin@ip-172-31-88-32:~$ which python3
/usr/bin/python3
admin@ip-172-31-88-32:~$ ls -l /usr/bin/pyth*
lrwxrwxrwx 1 root root       9 abr  5  2021 /usr/bin/python3 -> python3.9
-rwxr-xr-x 1 root root 5479736 feb 28  2021 /usr/bin/python3.9
lrwxrwxrwx 1 root root      33 feb 28  2021 /usr/bin/python3.9-config -> x86_64-linux-gnu-python3.9-config
lrwxrwxrwx 1 root root      16 abr  5  2021 /usr/bin/python3-config -> python3.9-config

Ahora si lo tenemos todo. Python3 es un enlace simbólico a /usr/bin/python3.9. Por tanto vamos a utilizar explícitamente este python3.9 para crear el entorno virtual:

admin@ip-172-31-88-32:~$ virtualenv -p /usr/bin/python3.9 webenv
created virtual environment CPython3.9.2.final.0-64 in 253ms
  creator CPython3Posix(dest=/home/admin/webenv, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy,   
   app_data_dir=/home/admin/.local/share/virtualenv)
   added seed packages: pip==20.3.4, pkg_resources==0.0.0, setuptools==44.1.1, wheel==0.34.2
  activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator

Ya lo deberíamos tener creado con el nombre de webenv en el directorio home del usuario:

admin@ip-172-31-88-32:~$ ls
webenv

Por último, vamos a activar el entorno:

admin@ip-172-31-88-32:~$ source webenv/bin/activate
(webenv) admin@ip-172-31-88-32:~$

Perfecto, el (webenv) antes del prompt nos indica que el entorno ha quedado activo y funcionando. Estamos listaos para instalar las dependencias, Django y todo lo que necesite nuestro proyecto.

3. Clonar el repositorio

Antes de continuar vamos a clonarnos el repositorio del blog. Primero hacemos un “deactivate” para desactivar el entorno virtual (aunque este paso creo que es irrelevante, ya que el clonado del repositorio en cualquier caso no quedará asociado al entorno virtual):

(webenv) admin@ip-172-31-88-32:~$ deactivate
admin@ip-172-31-88-32:~$
admin@ip-172-31-88-32:~$ git clone https://github.com/IsmaelB83/laestanciaazul.git
Clonando en 'laestanciaazul'...
remote: Enumerating objects: 9733, done.
remote: Counting objects: 100% (244/244), done.
remote: Compressing objects: 100% (165/165), done.
remote: Total 9733 (delta 106), reused 171 (delta 78), pack-reused 9489
Recibiendo objetos: 100% (9733/9733), 20.44 MiB | 26.87 MiB/s, listo.
Resolviendo deltas: 100% (3394/3394), listo.

Una vez tenemos el repositorio clonado, y nos hemos copiado nuestro passwords.json (ver instrucciones del repo para configurarlo en detalle), vamos a pasar a instalar todas las dependencias del fichero requirements.txt. Para eso es necesario volver a activar el entorno virtual previamente. Esto es MUY IMPORTANTE para no ensuciar de dependencias nuestra instancia. Ahora mismo nuestro entorno virtual debería tener unas dependencias mínimas:

(webenv) admin@ip-172-31-88-32:~$ pip list
Package       Version
------------- -------
pip           20.3.4
pkg-resources 0.0.0
setuptools    44.1.1
wheel         0.34.2
(webenv) admin@ip-172-31-88-32:~$

Accedemos a la ruta del proyecto donde está el fichero requirements.txt e instalamos las dependencias:

(webenv) admin@ip-172-31-88-32:~$ cd laestanciaazul/
(webenv) admin@ip-172-31-88-32:~/laestanciaazul$ pip install -r requirements.txt 
Collecting Django==4.1.4
  Downloading Django-4.1.4-py3-none-any.whl (8.1 MB)
     |████████████████████████████████| 8.1 MB 25.5 MB/s 
Collecting django-ckeditor==5.4.0
Downloading django-ckeditor-5.4.0.tar.gz (1.6 MB)
   |████████████████████████████████| 1.6 MB 35.7 MB/s 
Collecting django-widget-tweaks==1.4.12
Downloading django_widget_tweaks-1.4.12-py3-none-any.whl (8.9 kB)
Collecting django-wysiwyg==0.8.0
Downloading django_wysiwyg-0.8.0-py3-none-any.whl (23 kB)
Collecting Pillow==9.3.0
Downloading Pillow-9.3.0-cp39-cp39-manylinux_2_28_x86_64.whl (3.3 MB)
   |████████████████████████████████| 3.3 MB 23.1 MB/s 
Collecting psycopg2-binary==2.9.5
Downloading psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.0 MB)
   |████████████████████████████████| 3.0 MB 23.5 MB/s 
Collecting urllib3==1.26.5
  Downloading urllib3-1.26.5-py2.py3-none-any.whl (138 kB)
     |████████████████████████████████| 138 kB 42.3 MB/s 
Collecting sqlparse>=0.2.2
  Downloading sqlparse-0.4.3-py3-none-any.whl (42 kB)
     |████████████████████████████████| 42 kB 2.5 MB/s 
Collecting asgiref<4,>=3.5.2
  Downloading asgiref-3.5.2-py3-none-any.whl (22 kB)
Collecting django-js-asset
  Downloading django_js_asset-2.0.0-py3-none-any.whl (4.9 kB)
Building wheels for collected packages: django-ckeditor
  Building wheel for django-ckeditor (setup.py) ... done
  Created wheel for django-ckeditor: filename=django_ckeditor-5.4.0-py2.py3-none-any.whl size=2256438 sha256=6e7966d319b045764adbdb03b2ee16d5d5af285dc0a1af6c549b7c8a59bb4be9
  Stored in directory: /home/admin/.cache/pip/wheels/f5/1f/6c/5c892fd93990877b657249cc2e63bfc2a27667ceb7bf5871b5
Successfully built django-ckeditor

Installing collected packages: sqlparse, asgiref, Django, django-js-asset, urllib3, psycopg2-binary, Pillow, django-wysiwyg, django-widget-tweaks, django-ckeditor

Successfully installed Django-4.1.4 Pillow-9.3.0 asgiref-3.5.2 django-ckeditor-5.4.0 django-js-asset-2.0.0 django-widget-tweaks-1.4.12 django-wysiwyg-0.8.0 psycopg2-binary-2.9.5 sqlparse-0.4.3 urllib3-1.26.5

(webenv) admin@ip-172-31-88-32:~/laestanciaazul$

Todo listo, ya tenemos nuestro entorno “virtual” preparado.

4. Preparar instancia para aceptar conexiones remotas con public_key y crear usuario NO admin

En este punto me doy cuenta de que he realizado toda la instalación con el usuario “admin” el cual posee permisos de root de toda la instancia. Lo cual es una muy mala práctica de seguridad. Por tanto, voy a crear primero un usuario con permisos NO root, y volcaré a su directorio home toda la configuración realizada hasta ahora. Como se ha realizado la configuración mediante virtualenv. Es muy sencillo copiar el entorno.

admin@ip-172-31-88-32:~/laestanciaazul$ sudo adduser trama
Añadiendo el usuario `trama' ...
Añadiendo el nuevo grupo `trama' (1001) ...
Añadiendo el nuevo usuario `trama' (1001) con grupo `trama' ...
Creando el directorio personal `/home/trama' ...
Copiando los ficheros desde `/etc/skel' ...
Nueva contraseña: 
Vuelva a escribir la nueva contraseña: 
passwd: contraseña actualizada correctamente
Cambiando la información de usuario para trama
Introduzca el nuevo valor, o pulse INTRO para usar el valor predeterminado
                Nombre completo []: Ismael
                Número de habitación []: 
                Teléfono del trabajo []: 
                Teléfono de casa []: 
                Otro []: 
¿Es correcta la información? [S/n] S
admin@ip-172-31-88-32:~/laestanciaazul$ sudo usermod -aG sudo trama
admin@ip-172-31-88-32:~/laestanciaazul$ cd ..
admin@ip-172-31-88-32:~$ ls
laestanciaazul  webenv
admin@ip-172-31-88-32:~$ sudo mv laestanciaazul/ /home/trama/
admin@ip-172-31-88-32:~$ sudo mv webenv/ /home/trama/
admin@ip-172-31-88-32:~$ ls -a
.  ..  .bash_history  .bash_logout  .bashrc  .cache  .local  .profile  .python_history  .ssh
admin@ip-172-31-88-32:~$

NOTA: tras esto será necesario volver a hacer el pip install -r requirements.txt una vez logados con el nuevo usuario y teniendo la webenv activa.

5. Preparar instancia para aceptar conexiones remotas con public_key

En este punto también nos interesa habilitar la posibilidad de que el usuario trama se pueda conectar a la instancia por ssh sin necesidad de utilizar el fichero .pem generado. Que debería utilizarse únicamente para acceder con el usuario admin (en muy contadas ocasiones).

Para ello vamos a habilitar en el .ssh/authorized_keys la clave publica de nuestro equipo (no voy a entrar en explicar en este manual como generar una clave publica/privada. Hay varios manuales en internet para ver el proceso).

admin@ip-172-31-88-32:~$ cd .ssh/
admin@ip-172-31-88-32:~/.ssh$ ls
authorized_keys
admin@ip-172-31-88-32:~/.ssh$ nano authorized_keys 
admin@ip-172-31-88-32:~/.ssh$

Básicamente accedemos al fichero “authorized_keys” y añadimos tras la clave .pem generada por AWS, la clave publica de nuestro equipo para que pueda conectar sin necesidad de utilización del fichero .pem. Esto lo hacemos tanto para el usuario admin como para el nuevo usuario trama.

Ahora ya podemos conectar sin necesidad del fichero .pem:

ssh trama@ec2-54-172-3-252.compute-1.amazonaws.com 
trama@ip-172-31-88-32:~$ 

6. Preparar la base de datos

En este punto voy a crear la base de datos (vacía) y el usuario que tendrá acceso a la misma.

trama@ip-172-31-88-32:~$ sudo -u postgres psql
postgres=# CREATE DATABASE laestanciaazul;
CREATE DATABASE
postgres=# CREATE USER trama WITH PASSWORD 'xxxxxxxxx';
CREATE ROLE
postgres=# ALTER ROLE trama SET client_encoding TO 'utf8';
ALTER ROLE
postgres=# ALTER ROLE trama SET default_transaction_isolation TO 'read committed';
ALTER ROLE
postgres=# ALTER ROLE trama SET timezone TO 'UTC';
ALTER ROLE
postgres=# GRANT ALL PRIVILEGES ON DATABASE laestanciaazul TO trama;
GRANT
postgres=# \q
(webenv) trama@ip-172-31-88-32:~$

En este punto lo normal sería hacer un migrate y makemigrations con ./manage.py de Django, pero yo estoy haciendo montando el site desde otro equipo, por lo que voy a restaurar la base de datos que tengo de backup mediante pg_restore.

Para ello lo primero es utilizar sshfs para montar la estructura de ficheros remota en mi equipo (punto de montaje /media/awsdjango):

sshfs trama@ec2-54-172-3-252.compute-1.amazonaws.com:/home/trama /media/awsdjango

A partir de aquí puedo acceder a la estructura de ficheros de la instancia EC2 desde mi local. Pudiendo copiar los ficheros que necesite para poder luego trabajarlos desde la instancia. Esto es una alternativa sencilla y rapida al uso de un sftp o similar cuando tenemos montado SSH en el servidor remoto.

trama@ip-172-31-88-32:~$ pg_restore -d laestanciaazul laestanciaazul09122022.sql

6. Levantar el sitio en modo debug

En este punto estamos listos para hacer un “collectstatics” y levantar el sitio en modo debug con runserver.

(webenv) trama@ip-172-31-88-32:~/laestanciaazul$ python3 ./manage.py collectstatic
1266 static files copied to '/home/trama/cdn_static'.
(webenv) trama@ip-172-31-88-32:~/laestanciaazul$ python3 ./manage.py runserver 0.0.0.0:8080
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
December 10, 2022 - 10:42:00
Django version 4.1.4, using settings 'web.settings'
Starting development server at http://0.0.0.0:8080/
Quit the server with CONTROL-C.