Mostrando entradas con la etiqueta git. Mostrar todas las entradas
Mostrando entradas con la etiqueta git. Mostrar todas las entradas

miércoles, 14 de septiembre de 2022

Curso Esquemático de Git y Github

imagen de entrada de git







Curso Esquemático de Git y Github.


Cuando pasas de un proyecto sencillo a otro más complicado, estas trabajando con más personas en un mismo programa o si simplemente quieres tener un registro de como está evolucionando el código que estas desarrollando para que un tú del futuro sepa lo que has hecho, tenemos que utilizar si o si un administrador de versiones. El más conocido en términos de rendimiento, seguridad y flexibilidad es Git y su plataforma GitHub.

Antes de meternos en materia de como funciona, es necesario entender como se pueden controlar las diferentes versiones de un programa. Si tuviéramos que hacer nosotros manualmente un control de como evoluciona un programa podríamos usar el comando "diff" de Linux de la siguiente forma.

Imaginemos un programa como el siguiente:

# version 0.1 
print("Hola Mundo")

Vamos a ponerle un nombre y guardarlo. Yo lo llamaré archivo_1.py. Ahora vamos a añadir una nueva línea:

# version 0.2 
print("Hola Mundo")
print("Esta es la segunda versión")

Lo vamos a guardar con un nombre distinto para poder comparar ambas versiones. Este se llamará archivo_2.py

Si en el shell de Linux tecleamos:

$ diff archivo_1.py archivo2_py

Veremos lo siguiente:

1c1
< # version 0.1 
---
> # version 0.2 
2a3
> print("Esta es la segunda versión")
Aunque se pueden intuir las diferencias existentes entre los archivos, el resultado no es fácilmente comprensible. Esto es debido a que fue diseñado para ser usado por computadoras y no por humanos. Pero a groso modo lo que viene a decir es que la primera línea del primer archivo debe cambiarse por la primera del segundo para que estén sincronizados. El símbolo "<" es el texto que sale y el ">" el que entra
Y que a la segunda línea del primer archivo que no cambia, hay que añadirle la tercera del segundo archivo.

Otras formas de hacer los mismo es usando el parámetro -u que compara ambos programas línea por línea.

$ diff -u archivo_1.py archivo2_py
--- archivo_1.py	2022-08-15 18:15:18.939411615 +0200
+++ archivo_2.py	2022-08-15 18:15:31.091518676 +0200
@@ -1,2 +1,3 @@
-# version 0.1 
+# version 0.2 
 print("Hola Mundo")
+print("Esta es la segunda versión")

Y quizá la más intuitiva es la que lo compara a dos columnas:

$ diff -y archivo_1.py archivo2_py
# version 0.1 			|# version 0.2 
print("Hola Mundo")		   print("Hola Mundo")
				>  print("Esta es la segunda versión")

Aplicando Cambios.

Para guardar los cambios que ha habido de la versión 0.1 del programa a la 0.2 usaríamos la siguiente instrucción:

$ diff -u archivo_1.py archivo2_py > cambios.diff

Si ahora, por ejemplo, se nos borra el archivo_2.py pero conservamos el archivo con las diferencias podemos volver a generarlo con las siguiente instrucción.

$ patch archivo_1.py < cambios.diff

Esto nos dará el archivo_2.py que se nos había borrado.

Esto es especialmente útil cuando tienes un programa que contiene un error y que tu no localizas. Imagina que se lo pasas a un amigo que te lo soluciona. Este amigo genera un nuevo archivo con las diferencias "cambios.diff" y te lo reenvía. Tu aplicas la solución ejecutando el comando "patch" tal como vimos en el ejemplo anterior sobre tu archivo.

IMPORTANTE: estos comandos se pueden aplicar también sobre directorios.

No obstante imagínate el lio que supondría aplicar estos métodos cada vez que hayas realizado modificaciones significativas en tus programas. Para hacerlo de forma automática por ti están los controladores de versiones como es el caso de Git.


Instalando GIT, configuración y uso.


1.- Instalando GIT.

Primeramente debemos comprobar si tenemos Git instalado. Para ello podemos usar:

$ git --version
git version 2.34.1

Esto nos dice que tenemos la versión 2.34.1 instalada. 

Si no lo tuviésemos instalado, en Linux una de las formas de hacerlo es con:

$ sudo apt-get install git

Si no usas un gestor de paquetes entonces hay que ir a su página web y seguir las instrucciones.

En Ubuntu además puedes considerar instalar estos dos paquetes que te pueden ayudar aunques son opcionales:

- Gitk 

Es un navegador gráfico que se usa para ver de manera gráfica el historial de un repositorio git.

Se instala con:

$ sudo apt-get install gitk

Ejemplo con $ gitk -all sobre un repositorio.

gtik ejemplo

- Git-gui

Es un entorno gráfico para usar los comandos de git sobre los repositorios.

Se instala con:

$ sudo apt-get install git-gui

Ejemplo de uso de $git gui

git gui



2.- CONFIGURANDO GIT.

Una vez instalado lo primero que tenemos que hacer es configurarlo con nuestro nombre y correo electrónico:

$ git config --global user.email  micorreo@ejemplo.com

$ git config --global user.name  mi_nombre

Usamos el parámetro "--global" porque queremos usar estos valores para todos los repositorios que creemos. Si queremos usar un correo y nombre especifico para un proyecto concreto, dentro de ese proyecto usaremos la mima instrucción pero con el parámetro "--local".

Si quieres ver como está la configuración actual de Git puedes usar el siguiente comando:

$ git config -l  o $ git config --list


3.- INICIANDO EL REPOSITORIO.

Dentro de la carpeta del proyecto hay dos formas de iniciar git.

Si empezamos el repositorio desde cero:

$ git init

Sin argumentos nos crea una carpeta oculta .git en la carpeta de trabajo donde iran todos los archivos que git necesita para funcionar. Si le pasamos como argumento un nombre entonces nos crea una carpeta con ese nombre y dentro crea la carpeta oculta .git.

Si lo que queremos es hacer una copia de un repositorio ya existente en otro lugar, entonces usaremos:

$ git clone dirección@delrepositorio


Antes de continuar hay que aclarar estos tres conceptos:

escenarios en git

El directorio
. Ahí están todos los archivos del proyecto tanto los que queramos realizar un seguimiento como los que no. Es el directorio de trabajo con todos sus archivos y directorios.

Working Tree. En este mismo directorio, a todo lo que queremos realizar un seguimiento para controlar sus cambios, tanto si son archivos concretos como directorios, es lo que va a formar el llamado árbol de trabajo. Esta lleno de archivo que editas, donde se agregan nuevos archivos y de los que eliminamos archivos innecesarios. Cualquier cambio en el árbol de trabajo, queda registrado y se muestra como archivos o directorios modificados.

Stage. Es el lugar donde se guardan los cambios, para luego ser llevados definitivamente al repositorio cuando hagamos instantáneas del proyecto llamadas confirmaciones o commits en inglés.

Vamos a ver un pequeño ejemplo de como crear un repositorio desde cero. Primero creamos el directorio de trabajo (proyecto), entramos en él y posteriormente iniciamos el repositorio.

creación de un repositorio desde cero


4.- Añadir archivos al árbol de trabajo o rastrearlos.

Si queremos ir añadiendo archivos uno a uno.

$ git add [nombre_del_archivo]

Pasa los archivos del directorio al staging area.

Si queremos añadir todo el directorio. (el punto al final aquí es importante, no es una errata)

$ git add .


primer archivo añadido



En el ejemplo anterior creamos un archivo nuevo llamado main.py, que antes no estaba. Quise ver en que estado estaba para git. 

Lo realizamos con la sentencia:

$ git status

Git nos dijo como estaba el árbol de trabajo y la situación actual del repositorio. También que estamos en la rama "master", que aún no hemos hecho ningún "commit" y que tenemos un archivo sin seguimiento, que es el main.py vacío que hemos creado.

Lo añadimos al seguimiento con "git add" y como vemos después, otra vez con git status, el archivo aparece ahora en verde y está listo para acciones posteriores.


5.- Pasar los cambios del Working Tree al Stage.


Ahora que ya tenemos un repositorio git, lo hemos configurado y hemos creado un archivo. El siguiente paso es realizar algunos cambios en el mismo y confirmar instantáneas de esos cambios en el repositorio cada vez que el proyecto alcance un estado que quieras conservar.  Tendremos que poner todo lo que queramos que se conserve en el Stage Area "Escenario" para poder realizarle una instantánea.

Abre el archivo main.py e incluye algún comentario o algo de texto. 

Ahora queremos conservar nuestro proyecto tal como esta en este momento, con el comentario o texto que acabamos de añadir. Como hemos modificado el archivo que ya teníamos rastreado, si hacemos un "git status"  el programa nos avisa que tenemos un archivo que ya teníamos rastreado pero que hemos hecho cambios. Como queremos hacer la instantánea del archivo con el comentario nuevo introducido tenemos nuevamente que confirmar ese cambio para que el archivo pase del área de trabajo al Escenario (Stage).

Hacemos nuevamente:

$ git add main.py

Ahora si que podemos tomar la instantánea del proyecto ya que el archivo está en el staging area. Para conservar el repositorio tal como está en este momento y hacer la instantánea o foto del proyecto en su estado actual.

El comando a utilizar es:

$ git commit

Como ves este comando toma los archivo, directorios etc que están en el área de trabajo y crea el repositorio.

Al hacerlo se abrirá tu editor de texto por defecto, el mío en Ubuntu es nano y podrás escribir de forma detallada en que consisten las modificaciones, para que tu yo del futuro o cualquiera que lo lea pueda saber lo que se ha hecho.

El editor por defecto se puede modificar con:

$ git config --global core.editor <nombre_editor>                   (nano, vim, emac etc)

Existen unas reglas de estilo a seguir que brevemente son:

- La primera línea se pondrá una breve descripción de no más de 50 caracteres de lo que se ha hecho desde el último commit.

- Luego añadimos una descripción ya más detallada del mismo.

primer commit

Guardamos y ya está. 

Nota: Si no escribes nada o sales se aborta el commit.

Si el repositorio es para tí y no estás muy interesado en realizar una descripción detallada de los cambios puedes utilizar el comando:

$ git commit -m "texto con la descripción abreviada del cambio"

Por ejemplo:

$ git commit -m "Primer commit"

No te saldrá el editor de texto y el commit se guardará con el texto introducido.


Otro atajo para realizar los commits, puede ser también:

$ git commit -a 

Es un atajo para confirmar cualquier cambio en los archivos que tengamos rastreados y confirmarlos en un solo paso. Pero ¡Ojo! no realiza commit de los archivos nuevos, solamente de los que estén en seguimiento, es decir de aquellos que hayamos hecho un "git add [archivo]" primero.

Es una mezcla de "git add . + git commit"

Luego, para ver los comentarios de los commits usaremos el argumento "log". Hay varios parámetros que podemos usar.

El más sencillo es:

$ git log

chema@lenovo:~/proyecto$ git log
commit 168d6f4f8baa5c29310309b82c5e7b9232ff877d (HEAD -> master)
Author: usuario <usuario@correo.es>
Date:   Fri Sep 9 19:33:22 2022 +0200

    Proyecto inicial para aprender git básico
    
    Este es el commit o primera instantanea del proyecto. Hemos iniciado un
    repositorio, creado un archivo. Luego hemos añadido ese archivo al segui-
    miento de git. Añadimos un comentario volvimos a confirmar los cambios y
    realizamos el primer commit.

$ git log --stat

Muestra estadísticas de los commits.

chema@lenovo:~/proyecto$ git log --stat
commit 168d6f4f8baa5c29310309b82c5e7b9232ff877d (HEAD -> master)
Author: usuario <usuario@correo.es>
Date:   Fri Sep 9 19:33:22 2022 +0200

    Proyecto inicial para aprender git básico
    
    Este es el commit o primera instantánea del proyecto. Hemos iniciado un
    repositorio, creado un archivo. Luego hemos añadido ese archivo al segui-
    miento de git. Añadimos un comentario volvimos a confirmar los cambios y
    realizamos el primer commit.

 main.py | 1 +
 1 file changed, 1 insertion(+)

$ git log -p 

Nos indica los cambios entre commits, equivalente a la salida de la instrucción "diff -u" que vimos al principio del capitulo. Página la salida.

chema@lenovo:~/proyecto$ git log -p
commit 168d6f4f8baa5c29310309b82c5e7b9232ff877d (HEAD -> master)
Author: usuario <usuario@correo.es>
Date:   Fri Sep 9 19:33:22 2022 +0200

    Proyecto inicial para aprender git básico
    
    Este es el commit o primera instantánea del proyecto. Hemos iniciado un
    repositorio, creado un archivo. Luego hemos añadido ese archivo al segui-
    miento de git. Añadimos un comentario volvimos a confirmar los cambios y
    realizamos el primer commit.

diff --git a/main.py b/main.py
new file mode 100644
index 0000000..1274ace
--- /dev/null
+++ b/main.py
@@ -0,0 +1 @@
+# comentario

$ git log --oneline

Muestra cada commit que hayamos hecho en una sola línea, con lo que es más fácil de ver.


$ git show [id]

Lo mismo que el comando anterior pero mostrando solamente el último commit realizado. Si se introduce un ID concreto nos muestra solamente ese. El ID es el número largo que aparece después del commit, (en el siguiente ejemplo el ID es 168d6f4f8baa5c29310309b82c5e7b9232ff877d. Se puede usar abreviadamente los 6 primeros caracteres, en vez de teclear el número entero.)

chema@lenovo:~/proyecto$ git show
commit 168d6f4f8baa5c29310309b82c5e7b9232ff877d (HEAD -> master)
Author: usuario <usuario@correo.es>
Date:   Fri Sep 9 19:33:22 2022 +0200

    Proyecto inicial para aprender git básico
    
    Este es el commit o primera instantanea del proyecto. Hemos iniciado un
    repositorio, creado un archivo. Luego hemos añadido ese archivo al segui-
    miento de git. Añadimos un comentario volvimos a confirmar los cambios y
    realizamos el primer commit.

diff --git a/main.py b/main.py
new file mode 100644
index 0000000..1274ace
--- /dev/null
+++ b/main.py
@@ -0,0 +1 @@
+# comentario

$ git diff (archivo) 

Equivale al comando "diff -u" que vimos al principio, para comparar las diferencias entre dos versiones del mismo archivo. Nos muestra los cambios en los archivos en seguimiento PERO teniendo en cuenta el último commit y antes de realizar uno nuevo. Se le puede especificar tambien el nombre de un archivo en concreto.

$ git add -p  

Para confirmar las modificaciones realizadas a los archivos, antes de hacer el commit. En vez de realizar las confirmaciones de los cambios en los archivos de forma automática nos muestra los cambios y nos permite confirmarlos o no. Es como "git add" pero nos permite ver antes los cambios y confírmalos para añadirlos al stage.

chema@lenovo:~/proyecto$ git add -p
diff --git a/main.py b/main.py
index 3808744..91479a0 100644
--- a/main.py
+++ b/main.py
@@ -1,2 +1,2 @@
 # comentario

(1/1) Stage this hunk [y,n,q,a,d,e,?]? 

$ git diff --staged

Para ver los cambios en el stage pero que aún no tienen commit.


 

lunes, 7 de junio de 2021

Flask 21. Desplegar una aplicación Flask en Heroku.

logotipo de heroku


Anteriormente. Apéndice 20. Uso de MYSQL o MARIADB en un proyecto Flask.


En este capítulo vamos a desplegar nuestra aplicación en Heroku, que es un servidor de alojamiento en la nube externo. Muchos proveedores de alojamiento en la nube ofrecen una plataforma administrativa en la que se pueden ejecutar aplicaciones. 

Todo lo que necesitamos para implementar una aplicación de Python en estas plataformas es la aplicación solamente, porque el hardware, el sistema operativo, los interpretes de lenguaje de secuencias de comandos, las bases de datos etc son todos administrados por el servicio. Este tipo de servicio se denomina Plataforma como servicio o PaaS.

Heroku, que es un servicio muy popular, nos facilita además una serie de servicios de forma gratuita en donde podremos implementar nuestra aplicación de Python.


Hosting en Heroku.


La implementación de una aplicación Python en Heroku se realiza a través de la herramienta de control de versiones de git, por lo que deberemos tener nuestra aplicación en un repositorio de git. Heroku busca un archivo llamado Procfile en el directorio raíz de la aplicación para obtener instrucciones sobre como iniciar la aplicación. Para los archivos de Python, Heroku también espera un archivo llamado "requirements.txt" que contendrá todas las dependencias del modulo que deben instalarse. Una vez que nuestra aplicación se cargue en los servidores de Heroku a través de git, solamente tendremos que esperar unos segundos hasta que la aplicación este en línea y funcionando. Así de simple.

Lógicamente al ser un servicio gratuito, que nos facilitan para desarrollar y practicar la programación, la capacidad de computo y almacenamiento es limitada. Si necesitas mayor potencia tendrás que adquirirla a través de lo que Heroku llama "dynos". Pero para lo básico y para practicar, el servicio gratuito nos servirá de sobra.


Crear una cuenta en Heroku.

Antes de poder empezar, lógicamente tenemos que tener una cuenta con ellos. Visita www.heroku.com y crea una cuenta gratuita.

página de inicio de Heroku

Una vez que tengas la cuenta, inicia sesión con tu correo y password (Log in) y tendrás acceso a un panel de administración donde aparecerán nuestros proyectos.

vista de nuevo proyecto en heroku



Instalando el cliente de Heroku en el ordenador.


Heroku nos proporciona una herramienta de línea de comandos para poder interactuar con sus servicios llamada Heroku CLI, que tenemos disponible para Linux, Mac y Windows. En la documentación tenemos las instrucciones de instalación para cada una de las plataformas compatibles. 

página de descargas de Heroku



Como yo esto trabajando con Ubuntu usaré la siguiente:

$sudo snap install --classic heroku

pero también puedes usar la que aparece en su página web más abajo para Debian, que es la que nos funcionará en la Raspberry Pi.

Nota: En ubuntu una instalación alternativa si te diese problemas de actualización (heroku update) de paquetes de snap es curl https://cli-assets.heroku.com/install.sh | sh. En este caso el programa está en /usr/local/bin/heroku

Desinstala previamente el paquete snap con sudo snap remove heroku.

Sigamos.

Lo primero que tenemos que hacer es loguearnos en la cuenta de heroku que creamos antes. Para ello tecleamos en el terminal:

$ heroku login
Heroku CLI abrirá el navegador y nos llevara a una página donde nos loguearemos. Nos pedirá la dirección de correo con la que nos registramos y nuestro password de la cuenta. Una vez hecho esto podemos cerrar la página que se nos ha abierto en el navegador y si vamos al terminal veremos que aparecemos logueados. Esta acción no hace falta volver a hacerla porque se recordará para comandos posteriores.

Preparando la aplicación para subirla a Heroku.


Aunque ser pude hacer de múltiples formas yo voy a usar la siguiente. Primeramente crearemos un esquema de directorios donde poner el proyecto con la siguiente forma:


Esquema de directorios y archivos del proyecto


1.- Creo una carpeta que se llame proyecto y entro en la misma. 

$ mkdir proyecto
$ cd proyecto
proyecto $
2.- Creo en el entorno virtual que usará la aplicación:

proyecto $ python3 -m venv venv

3.- Creamos la carpeta src que contendrá el código fuente del proyecto.

proyecto $ mkdir src

Dentro de esta carpeta colocaré los archivos del programa de Flask que hemos utilizado a lo largo de este tutorial y que puedes encontrar en esta carpeta del proyecto en github..

Para descargar solamente este directorio y no todo el repositorio, entras en el directorio POST 19, copias el enlace del navegador y puedes utilizar alguno de estos 2 links que te dejo:

https://downgit.github.io/#/home

https://download-directory.github.io/

para obtener un archivo zip con el código fuente. Luego extraes los archivos dentro de la carpeta src.

La carpeta src debería quedar tal como se ve a continuación.


directorios y archivos del proyecto en el directorio padre scr


IMPORTANTE: al tratarse de un entorno de producción asegúrate de entrar en la carpeta config, editar el archivo config.py y poner el DEBUG=False.


Heroku necesita unos cuantos archivos para subir el proyecto y que funcione. Estos son:

requirements.txt  => Este archivo contiene todas las librerías o bibliotecas de Python que el programa utiliza y que son necesarias para su funcionamiento. Todos los paquetes que utilices para el proyecto deben estar aquí para que heroku sepa lo que se necesita. Este archivo en su día lo realizamos entrando en el entorno virtual y ejecutando la siguiente instrucción:

env $ pip freeze > requirements.txt

Aquí ya esta hecho y si lo abres verás todas las bibliotecas que se utilizan en el programa.

Nota: Si en alguna ocasión te sale dentro de requirements.txt el paquete pkg-resources==0.0.0 elimínalo que luego al subirlo a heroku da un error.

Como lo voy a necesitar posteriormente para pasar de una base sqlite a una postgresql (un poco más adelante veras porqué) vamos a instalar todos los paquetes o dependencias del proyecto en local. Para  instalarlos en nuestro ordenador tenemos que entrar en el entorno virtual, en el directorio src que contiene requirements.txt y ejecutar la siguiente instrucción. Vamos a hacerlo:

env $ pip install -r requirements.txt


[runtime.txt] => Es un archivo en el que se recoge la versión de Python del programa. Este es un archivo opcional y sino lo creamos el programa se ejecutará con la versión 3.9.5. No obstante podemos elegir otra versión distinta. Mira el manual ya que hay que escribir en el archivo la versión de una forma particular y con los tres dígitos, ya que sino no funcionará. Para saber que versión de Python estamos tenemos simplemente tecleamos:

>>> python3 --version
Python 3.8.5


Procfile => En este fichero se recoge que archivo se debe ejecutar en Heroku cuando la aplicación se inicie.

En Heroku se necesita para que cualquier proyecto de Flask funcione instalar el complemento gunicorn que ejecuta el servidor http. Como este paquete en el proyecto no estaba instalado necesitamos hacerlo. Para ello entraremos en el entorno virtual (desde el directorio src, $ source ../venv/bin/activate) y ejecutaremos la siguiente instrucción:

(env) /proyecto/src $ pip install gunicorn

Luego creamos el archivo Procfile y escribimos:

web: flask db upgrade; gunicorn inicio:app

Guardamos y salimos. 

inicio es el nombre del archivo de python que ejecuta la aplicación (inicio.py) y app hace referencia al módelo que lanza la aplicación dentro del mismo (app = Flask(__name__)). "flask db upgrade" lo utilizaremos para consolidar la migración de nuestra base de datos sql a la que utiliza heroku como veremos más adelante en el capitulo.

Como esta biblioteca no estaba instalada en su día, en el archivo requirements.txt tenemos que añadirla. Para ver la versión que se nos ha instalado tecleamos:

(env) ~/proyecto/src $ pip freeze
gunicorn==20.1.0

Pues tal cual copiamos la salida "gunicorn==20.1.0" y la añadimos al final del archivo requirements.txt. 

paquetes contenidos en el archivo requeriments.txt


Guardamos y salimos.

Es sistema de archivos es temporal.


En Heroku tenemos un problema. En cualquier momento se puede restablecer el servidor virtual en el que se ejecutará nuestro programa a su estado inicial. Esto ocasiona que no se pueda confiar en que los datos que se guarden en el sistema de archivos persistirán, ya que de hecho, Heroku restablece los servidores con bastante frecuencia.

El problema que se genera es que el motor de la base de datos SQlite escribe datos a un archivo en disco, que se borraría al reiniciarse el servidor al igual que todo lo que se almacene en memoria. Con lo que si se hubieran escrito nuevos registros estos se borrarían dejando la base de datos sql a su estado original.

Afortunadamente lo que si nos proporciona Heroku, con un complemento, es una base de datos propia Postgresql lo que nos solucionará el problema de la persistencia de los datos.

Trabajando con la base de datos Postgresql de Heroku.


En este punto nos encontramos con un segundo problema. Tenemos la base de datos de los usuarios en formato sqlite, la cual no es soportada por Heroku por lo que hemos comentado anteriormente. Por lo cual tenemos que usar una base con un motor de datos diferente. Heroku tiene una oferta de base de datos propia, basada en la base de datos Postgresql que vamos a utilizar. Escogeremos la que nos ofrece de forma gratuita que tiene un tamaño máximo de 8 MB y 20 conexiones simultáneas lo cual es más que suficiente para nuestro proyecto.

Para hacer esto, lo primero que tenemos que hacer es crear nuestra aplicación de Heroku. Aunque se puede hacer desde la pagina web, lo haremos desde el terminal del sistema con la instrucción:

$ heroku create nombre_proyecto_heroku
* Si el nombre_proyecto_heroku ya esta cogido te dará un error y tendrás que ponerle otro nombre distinto, el que más te guste. Yo lo he llamado novatillo.

y para añadirle una base de datos gratuita teclearemos:


heroku addons:create heroku-postgresql:hobby-dev --app nombre_proyecto_heroku


En mi caso la instrucción es como sigue:

$ heroku addons:create heroku-postgresql:hobby-dev --app novatillo
Creating heroku-postgresql:hobby-dev on ⬢ novatillo... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-cylindrical-56001 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation

La url de la base de datos se almacena en heroku en una variable de entorno llamada DATABASE_URL que estará disponible cuando se ejecute la aplicación. Pero aun que ya tenemos la base de datos tenemos que adaptar el conector en nuestro programa para poder usarla.


Conectando la base de datos de Heroku a nuestra aplicación Python.

Para usar PostgreSQL como nuestra base de datos en una aplicación de Python lo primero que tenemos que hacer es instalar el paquete psycopg2 en el entorno virtual, que es como el driver para que sqlalchemy pueda relacionarse con la la base postgrepsql que utiliza heroku:

(venv) ~/proyecto/src $ pip install psycopg2-binary

y como es un paquete nuevo que también es necesario para que nos funcione el proyecto en heroku tenemos que añadirlo al final del archivo requirements.txt

paquetes necesarios para instalar heroku, requeriments.txt


Ahora lo que tenemos que hacer es modificar el conector en nuestro programa para que SQLAlchemy se pueda comunicar con la base de datos de heroku una vez que lo hayamos subido a su servidor . Este conector es una variable del sistema llamada DATABASE_URL por lo que tenemos que capturarla y usarla en nuestro código tal como dice el manual de Heroku. 

Para saber cual es el conector con la base de datos de heroku podemos hacerlo desde la pagina web de administración de proyectos de heroku o desde el terminal usando la siguiente instrucción:

heroku config:get DATABASE_URL -a nombre_aplicación


# Como mi aplicación se llama novatillo  
(venv) ~/proyecto/scr $ heroku config:get DATABASE_URL -a novatillo
salida por pantalla del resultado de conectarnos a la base de datos de heroku

Copiamos todo esto que nos sale, que es la uri del conector.

Ahora tendríamos que entrar en config.py dentro de la carpeta config y modificar la línea 17 para capturar la variable de entorno en donde esta la base de datos en Heroku. Le tendríamos que decir al programa, que el conector  se encuentra primero en la variable del sistema DATABASE_URL y si no es así que use la antigua de sqlite.

Sin embargo por la nota siguiente voy a usar la uri que acabamos de copiar, y la pegaremos directamente. Ya que es un string la pegaremos dentro de un par de comillas " ".  Como ves la cadena comienza por "postgres:// ", pues bien cambia la palabra "postgres" por "postgresql". Quedarías así:


código del archivo config.py


¿Por qué? Lee lo siguiente:


Nota importante a la fecha en la que estoy escribiendo esto.


 El dialecto predeterminado que utiliza heroku en su base de datos es postgres y así nos lo pasan en DATABASE_URL. Sin embargo en un cambio reciente en la líbrería SQLAlchemy en la intrucción SQLALCHEMY_DATABASE_URI se espera encontrar un conector que comience por 'postgresql' para manejar este tipo de base de datos. De ahí que utilice todo el contenido de la variable y la modifique, y no capture solamente el valor de las misma. Es necesario para que la cosa funcione. Esto no quiere decir que en un futuro heroku pueda actualizar su complemento de postgres a postgresql ya no siendo necesario realizar este cambio. Si lo dejáramos tal cual tenemos este bonito error:
sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres
Otra posible solución es utilizar una libreria de SQLALchemy inferior a la versión 1.4.0  (1.3.23 por ejemplo debería funcionar)


Nos queda otra cuestión por resolver. Hasta ahora la base de datos que teníamos era sqlite y ahora necesitamos pasar su estructura a otro sistema: PostgreSql. Para ello tenemos que migrar la base de datos. Esto lo puedes ver en más detalle en el capitulo 14, así que aquí no lo explicaré y solo pondré los pasos:

Empezamos instalando el paquete flask-migrate en nuestro entorno virtual:

(venv) ~/proyecto/src $ pip install flask-migrate

y añadimos el paquete al archivo requirements.txt (Flask-Migrate==3.0.1)

Ahora en el archivo principal de la aplicación inicio.py tenemos que importar Migrate desde flask_migrate:

código del archivo inicio.py

y declararla después de db=SQLAlchemy(app)

insertar migrate en el archivo de inicio de Python

El proceso de migración es sencillo. Desde el terminal del entorno virtual inicializamos la migración. Entramos en el directorio src, si no lo estamos ya. Lo primero le decimos a Flask cual es la aplicación principal, más tarde haremos lo mismo en heroku. Lo hacemos creando una variable de entorno.


(venv) ~/proyecto/src $ export FLASK_APP=inicio.py
Seguidamente iniciamos la migración con:

(venv) ~/proyecto/src $ flask db init

iniciando migración en Python con flask db init


y realizamos la primera migración:

(venv) ~/proyecto/src $ flask db migrate -m "preparando para heroku"

primera migración

Si te fijas dentro del directorio src que tenemos en local se ha creado un nuevo directorio llamado "migrations". 

El último paso de la migración se realizará directamente cuando se ejecute la aplicación en heroku ya que si recuerdas al principio del capitulo creamos un archivo llamado Procfile, en el que usamos la última instrucción que necesitamos "flask db upgrade". Esta creará las tablas en la base de datos de heroku y pasará automáticamente los datos que tengamos en ella.

web: flask db upgrade; gunicorn inicio:app


Creación del repositorio git, configuración y subida del proyecto a heroku.


Para subir nuestro programa a Heroku hay que crear un repositorio de git. 

Claro esta que si no lo tienes instalado lo primero es descargarlo y configurarlo. Si no lo tienes ve al paso 1) y si ya lo tienes pasa al 2)

1) Ve a la página de github y create una cuenta (sign up). Te pedirá un usuario y una cuenta de correo.

Para instalar el cliente git en el ordenador, en una pi que usa Debian, o en ubuntu como estoy yo usamos:

$ sudo apt-get install git

Una vez instalado el cliente de git tenemos que configurar nuestro nombre de usuario y dirección de correo electrónico.
$ git config --global user.name "nombre_usuario"
$ git config --global user.email tucorreo@correo.com

Si quieres ver tu configuración puedes usar la siguiente orden en el terminal:

git config --list


2) Nos metemos en la carpeta src, si no lo estamos ya y tecleamos. No hace falta estar dentro del entorno virtual pero si es importante estar en la carpeta src.

$ git init

Inicia el repositorio.

$ git status 

Vemos los archivos preparados para subir (en rojo)

$ git add .

Le decimos a git que añada todo lo que hay en el directorio en el que estamos (src) al repositorio.

$ git commit -m "paquete git para subir a heroku"

y con esto creamos el repositorio. Si te fijas se ha creado un directorio llamado .git oculto dentro del directorio src. Ahí esta el repositorio. Si más adelante quieres borrar el repositorio solo tienes que borrarlo.

Ahora enlazamos la aplicación que hemos creado en Heroku (novatillo en mi caso) con la aplicación o repositorio de flask que tenemos en el ordenador.

# heroku git:remote nombre_aplicación
$ heroku git:remote novatillo

Todo esto lo hacemos dentro de la carpeta src.

Ahora que tenemos el proyecto enlazado lo subimos a heroku (¡por fin!)

$ git push heroku master

y si todo va bien, comprimirá la aplicación, subirá los archivos, instalará los paquetes y ya tendremos la aplicación subida y el link de nuestra aplicación en Heroku:

subida del proyecto a Heroku


Pero antes de acceder y como el primero de los dos subcomandos que usamos en el archivo Procfile

Procfile: Heroku Procfile.

web: flask db upgrade; gunicorn inicio:app

está basado en un comando de flask, al igual que hicimos cuando iniciamos la migración en local, necesitamos añadir la variable FLASK_APP al entorno de Heroku 

$ heroku config:set FLASK_APP=inicio.py

y ya esta todo. 

Si navegamos a la página que nos ha facilitado heroku https://(nombre_proyecto).herokuapp.com veremos nuestro proyecto funcionando en el servidor:

proyecto funcionando en Heroku



Anexo.

Otros comandos de heroku cli que nos pueden ser útiles son:

Establecer una variable de entorno en Heroku

$ heroku config:set FLASK_APP=inicio.py

Ejecutar un comando en la consola de la aplicación en heroku. En este caso una que nos muestre el historial de 
migraciones

# heroku run -a nombre_aplicación (orden a ejecutar)
$ heroku run -a novatillo flask db history

Podríamos haber hecho el upgrade de la migración no poniendo el comando automáticamente en el archivo Procfiles sino usando la consola de esta forma:

Upgrade de la migración

$ heroku run -a novatillo flask db upgrade


Próximo Capítulo. Flask 22. Desplegar una aplicación de Flask en un contenedor de Docker.

domingo, 8 de noviembre de 2020

Desplegar una aplicación Flask con Nginx y Gunicorn en una Rapsberry Pi

 








Anteriormente: Flask 19. Usar Flask Blueprint para estructurar aplicaciones. 2ª parte.

Desplegar una aplicación Flask con Nginx y Gunicorn en una Rapsberry Pi

En este capítulo, lo que vamos a hacer es desplegar nuestra aplicación Flask en una Rapsberry Pi que actuará como servidor, para que cualquiera pueda acceder a ella desde cualquier parte del mundo. Para ello utilizaremos los programas Nginx y Gunicorn. Gunicorn se encargará de traducir nuestro programa de Flask a Nginx para que este se encargue de servir nuestras plantillas web como respuesta a las peticiones que se realicen.

Paso 1: Requisitos previos

 A) Actualizar los repositorios del sistema.

El sistema operativo que utilizaré en mi Rapsberry Pi es el suyo propio, Raspbian Pi Os, que es un derivado linux de Debian. En él, ya viene instalado Python3 con lo que empezaremos actualizando los paquetes del sistema con los siguientes comandos:

$ sudo apt-get update

$ sudo apt-get full-upgrade


B) IP interna fija.

Es necesario que nuestra Raspberry Pi tenga una IP estática, que no varíe nunca cuando se reinicie, dentro de nuestra red local. 

Cada router es un mundo. Para configurar una IP estática debemos asegurarnos que esté fuera del rango DHCP (tendréis que verificarlo en la configuración de vuestro router) y que ningún otro dispositivo tiene asignada la misma IP. En el mío, se empiezan a asignar direcciones desde 192.168.1.1 hasta 192.1.168.199 por lo que no haya problemas de asignación, la Rapsberry Pi estará estática en la dirección 192.168.1.200

Para asignar una IP estática en Rapsberry Pi Os seguimos los siguientes pasos. Debemos editar el fichero /etc/dhcpcd.conf con el comando:

sudo nano /etc/dhcpcd.conf

En este fichero hay unas líneas comentadas, que empiezan por #. Buscamos la que tiene un ejemplo de configuración de IP estática (Example static IP configuratión) y añadimos la dirección estática que queramos (en mi caso 192.168.1.200/24, con el /24 al final) y la dirección de nuestro router, en el mío 192.168.1.1

Mi Rapsberry Pi esta conectada directamente al router (eth0), si la tienes conectada por wifi debes utilizar entonces "interface wlan0"

# Example static IP configuration:
interface eth0
static ip_address=192.168.1.200/24
static routers=192.168.1.1

Guardamos y Cerramos.

ACTUALIZACION.!!! A fecha de hoy 20/03/2024 con las sucesivas actualizaciones del sistema operativo de la pi, esto ya no funciona, concretamente desde la versión Bookworm. Ahora hay que hacerlo a nivel de router que es lo recomendado. No obstante se puede seguir haciendo a nivel de host de forma sencilla.  Sigue las instrucciones de este enlace https://access.redhat.com/documentation ... connection.

Si no eres root acuérdate de usar sudo delante. Se resume en dos pasos:

1.- Si no conocemos el nombre del dispositivo de red que estamos usando para la conexión, podemos mostrarlo con:

# nmcli device status

eth0           ethernet  connected               Wired connection 1 
lo             loopback  connected (externally)  lo                 
wlan0          wifi      disconnected            --                 
p2p-dev-wlan0  wifi-p2p  disconnected            --   

2.- Ejecutamos nmtui:

# nmtui

De forma visual, escogemos "Edit a conexion", seleccionamos la conexión activa por su nombre, en mi caso "Wired connection 1" y le damos a "Edit". Vamos al apartado "IPv4 CONFIGURATION" y en "Addresses" ponemos la ip fija que queremos utilizar y en "Gateway" la dirección de acceso al router. Le damos a OK, guardamos y salimos. 

Puedes encontrar más información en el forum de rapsberri pi "Static IP con Bookworm".

C) Un poco de seguridad.

Como norma básica de seguridad sería conveniente seguir una serie de pautas como cambiar el nombre del usuario por defecto, el nombre de la máquina, configurar un Firewall etc. 

NOTA: Por razones prácticas en el resto del capítulo (todo lo que no es este apartado de seguridad) he seguido usando el usuario pi y rapsberry como nombre del host, pero lo suyo seria cambiarlos. 

Cuando el servidor este en modo de producción tendríamos que haber seguido al menos las normas más básicas de seguridad que os paso a enumerar. Puedes encontrar más información aquí

1) Root desactivado.

En el sistema operativo que utilizo en la Raspberry Pi, Raspberry Pi OS o Raspbian antiguamente, el usuario Root ya viene desactivado por defecto. No obstante si utilizas otra distribución Linux que no ocurra esto deberías definir un usuario con permisos de Root  y luego desactivarlo.

2) Cambiar la contraseña por defecto del usuario pi.

Aunque lo mejor es utilizar otro usuario que no se pi, lo que haremos en el próximo paso, si vas a seguir con el usuario "pi" por lo menos cámbiale la contraseña que viene por defecto. Para ello desde el terminal puedes utilizar la aplicación de configuración:

sudo raspi-config

y ya de paso cambiar también el nombre de la máquina o host que por defecto es "raspberrypi" o puedes usar este comando de linux aunque solamente para cambiar la contraseña por defecto "raspberry"

passwd


3) Eliminar el usuario pi y crear uno nuevo con permisos.

Como los principales ataques por fuerza bruta que vas a recibir en tu servidor van a intentar conectarse al usuario "pi" que saben que es el que viene por defecto al menos vamos a intentar complicarles algo las cosas. Para ello seguimos el ejemplo que viene en el manual de seguridad de la fundación Rapsberry creando un nuevo usuario con privilegios y eliminando finalmente al usuario pi.

Empezaremos creando un nuevo usuario, usa el nombre que quieras, en el ejemplo utilizaremos alice.

sudo adduser alice

Aquí nos pedirá que introduzcamos la contraseña para este nuevo usuario. Este usuario tendrá su directorio base en /home/alice

Luego le daremos los permisos necesarios para poder funcionar:

sudo usermod -a -G adm,dialout,cdrom,sudo,audio,video,plugdev,games,users,input,netdev,gpio,i2c,spi alice

Para ver si todos los permisos estan correctos utiliza:

sudo su - alice

Si no te sale ningún error es que todo está bien.

Una vez hecho esto cierra la sesión que teníamos abierta con el usuario pi o reinicia el sistema y vuelve a entrar en el mismo, en este caso usando el nuevo usuario creado "alice". Ahora por si queda algún proceso que utilice el usuario "pi" antes de borrarlo paramos sus procesos con:

sudo pkill -u pi

y finalmente podemos optar por:

- borrar al usuario pi pero dejando todos sus directorios intactos para lo cual tecleamos:

sudo deluser pi

o si es una instalación limpia nos cargamos al usuario pi y también todos sus directorios que no nos sirven para nada con:

sudo deluser -remove-home pi

Si utilizas este último comando, te saldrá un mensaje de advertencia diciéndote que el grupo pi no tiene más usuarios. El comando deluser ha borrado tanto al usuario pi como el grupo pi así que puedes ignorar esta advertencia tranquilamente y seguir para adelante.


4) Hacer que el comando "sudo" requiera contraseña.

Poner sudo delante de un comando hace que ese comando se ejecute como si fueras un superusuario, y en esta distribución por defecto no hace falta contraseña. Esto generalmente no es un problema, pero imagina que como la pi va a estar conectada a internet alguien logra entrar y pueda ejecutar comandos que requieran credenciales de superusuario. Pues tenemos un problema. Para evitar esto forzaremos al sistema a que cada vez que usemos "sudo" nos pida la contraseña.

Entramos en el terminal y tecleamos:

sudo visudo /etc/sudoers.d/010_pi-nopasswd

Te encontraras una línea para el usuario pi, parecida que no igual que la de abajo, que tenemos que cambiar por nuestro nombre de usuario y la siguiente configuración:

alice ALL=(ALL) PASSWD: ALL

Si tuviésemos otros usuarios con permisos de superusuario también tendríamos que cambiarles la configuracion a ALL=(ALL) PASSWD: ALL. lo cual no es el caso si has seguido el ejemplo con una instalación limpia. Solo tenemos a alice. Guardamos y salimos.

Para que el cambio surja efecto reinicia el sistema.


5) Tener el sistema actualizado.

Básico en todos los sistemas operativos para tener los últimos parches que surjan para tapar agujeros de seguridad. 

Esto conviene hacerlo regularmente y son los comandos:

sudo apt-get update
sudo apt-get full-upgrade

Como esto en un servidor no es muy práctico el hacerlo a mano todos los días seria conveniente el hacerlo de manera automática. Puedes usar para ello la aplicación cron de linux que permitirá ejecutar estos comandos el día y a la hora que los programes o utilizar soluciones de terceros como unattended-upgrades

Como yo en mi caso además me conecto con la raspberry no directamente sino desde mi pc a través de ssh, conviene también tener siempre al día este protocolo con:

sudo apt install openssh-server


6) Mejorando la seguridad de la conexión SSH

Si como es mi caso, no os estáis tecleando directamente en la raspberry sino que lo hacéis remotamente a través de una conexión ssh desde un pc es muy importante seguir las siguientes normas para intentar evitar que alguien se nos cuele en el sistema por esta vía.

Lo más importante que debes hacer es asegurarte de tener una contraseña muy sólida. Si tu raspberry va a estar conectada a internet, haciendo de servidor, lo lógico es que tu contraseña de acceso sea lo más robusta posible. Esto te ayudará a evitar los ataques por fuerza bruta que por ejemplo usan un diccionario de palabras para intentar encontrar tu contraseña. 

Existen muchas páginas online donde puedes comprobar la robustez de una contraseña. Unas de ellas es por ejemplo https://password.kaspersky.com/es/

Si tecleas  por ejemplo 1234, verás lo siguiente:

No tienen que ser tampoco muy rebuscadas, (ojo que si metes números, mayúsculas y símbolos raros pues mejor) si utilizas dos palabras que te digan algo con un símbolo por medio, pues también está bien. Por ejemplo poniendo como contraseña "paragüas&rojo":


Si queremos añadir un poquito más de seguridad también podemos permitir o denegar a determinados usuarios su conexión a la raspberry por sshd. Como en mi caso, solo voy a querer entrar yo en la raspberry, me permitiré solamente el acceso a mi usuario mismamente :-)

Esto se hace entrando en el archivo de configuración con el comando:

sudo nano /etc/ssh/sshd_config

y añadiendo, editando o agregando al final del mismo la siguiente línea, que contiene los nombres de los usuarios a los que quiero permitir el acceso, en este caso yo mismo. Como en todo este subapartado de seguridad yo soy el usuario "alice" habría que añadir:

AllowUsers alice 

...vamos que solo permito al usuario "alice", que soy yo, el acceso a la raspberry mediante una conexión ssh.

También puedes usar DenyUsers para evitar específicamente que algunos nombres de usuario inicien sesión si te interesa:

DenyUsers mihermano hackermaligno loqueseteocurra

Después de guardar los cambios y salir del archivo si queremos que estos entren ya mismo en funcionamiento tenemos que reiniciar el servicio con:

sudo systemctl restart ssh

o también reiniciar la raspberry valdría para que los cambios tuvieran efecto.


AUNQUE LO MEJOR Y LO MÁS SEGURO,  que la gente que sabe de esto recomienda para acceder a la raspberry por ssh es que te olvides de la contraseña y utilices la autentificación basada en claves criptográficas (Key-based authentication)

Vamos con ello.

En el cliente, es decir en el ordenador que estas usando para conectarte a la raspberry hay que teclear desde el terminal:

>> ssh-keygen

Esto nos generará dos claves criptográficamente seguras, una privada y una pública que utilizaremos para autenticar nuestro ordenador (cliente) en un servidor (nuestra  pi) a través del prótocolo ssh.

Al ejecutar el comando nos saldrá la opción de poner contraseña aunque por el momento lo dejaremos en blanco. Creará un directorio oculto .ssh con la clave privada id_rsa y pública id_rsa.pub. La clave privada la mantendremos a buen recaudo mientras que la clave pública la tenemos que copiar a la raspberry pi con el siguiente comando, que tecleamos en la terminal de nuestro pc:

>>> ssh-copy-id <USERNAME>@<IP-ADDRESS>

<USERNAME> es el usuario de tu raspberry (alice en mi caso) y <IP_ADDRESS> la dirección de red que corresponde a tu raspberry (192.168.1.200 en mi caso)

Nota. Como lo que estamos haciendo es enviar la clave pública desde nuestro ordenador al fichero de claves autorizadas en la raspberry pi (authorized-keys) necesitaras teclear cuando te lo pida la contraseña de tu usario en la pi.


Yo lo estoy haciendo en una máquina con ubuntu 20.04 pero si este comando no estuviera disponible en vuestra distribución puedes copiarla directamente usando:

cat ~/.ssh/id_rsa.pub | ssh <USERNAME>@<IP-ADDRESS> 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'


También podrías enviarla manualmente a través de ssh con el comando scp. Puedes ver este link para obtener más información sobre como enviar archivos mediante el comando scp. 

Para finalizar como nos vamos a conectar usando las claves criptográficas ya no necesitamos que la rapsberry nos pida contraseña al conectarnos mediante ssh. Entonces para finalizar desactivamos que se requiera contraseña para conectarnos a la raspberry. ESTO lo tecleamos ya en la raspberry, para editar el archivo de configuración de la conexión ssh:

sudo nano /etc/ssh/sshd_config

Hay 3 líneas que necesitamos cambiar a no, si aún no están configuradas de esta manera.

ChallengeResponseAuthentication no
PasswordAuthentication no
UsePAM no

Guardamos y salimos.

Como siempre para que vuelva a funcionar reiniciamos el servicio con:

sudo service ssh reload

o reiniciamos el sistema.


7) Desabilitar el acceso como Root.

Aunque en Raspberry Os el root está deshabilitado por defecto no está de mas impedir el acceso como root al sistema. Solo nos llevará un momento y se hace en el mismo archivo de configuración que hemos usado antes:

sudo nano /etc/ssh/sshd_config

Buscamos la línea y la cambiamos a no, o si no existe lo tecleamos directamente:

PermitRootLogin no

Guardamos y salimos.

Igual que en el punto anterior reiniciamos el servicio con:

sudo service ssh reload

o reiniciamos el sistema.


8) Configurar un Firewall Básico.

Como nuestro servidor va a estar abierto al mundo exterior es conveniente que tengamos algún tipo de Firewall UFW. Rapsberry Pi no tiene ninguno instalado con lo que procedemos a instalarlo con:

alice@raspberrypi:~ $ sudo apt-get install -y ufw

Cuando usamos un sistema en remoto hay que tener cuidado ya que es muy fácil bloquearse a uno mismo y no poder entrar en el sistema. Por ejemplo porque usemos la conexión ssh y hayamos configurado el firewall para que lo bloquee.

Por ello lo primero que vamos a ver es como resetear el firewall a su configuración original por si algo sale mal. Lo que tendrías que teclear, que ahora no hace falta es:

alice@raspberrypi:~ $ sudo ufw reset

Si como yo, no tienes monitor y estás accediendo a la rapsberry pi a través del protocolo ssh, necesitamos asegurarnos de que el cortafuegos permita conexiones ssh para que podamos conectarnos o iniciar sesión con ella la próxima vez. Podemos permitir esas conexiones tecleando:

alice@raspberrypi:~ $ sudo ufw allow OpenSSH

o también podríamos utilizar otra forma alternativa que hace los mismo:

alice@raspberrypi:~ $ sudo ufw allow ssh 

o tambien alice@raspberrypi:~ $ sudo ufw allow 22

¿Por qué es lo mismo? Pues porque el servicio ssh utiliza por defecto el puerto 22. Si por más seguridad hubiéramos configurado el puerto por defecto que usa el servicio ssh a otro puerto distinto tendrías que abrir ese puerto para poder usar el servicio.

Posteriormente ya podemos activar el cortafuegos tecleando:

alice@raspberrypi:~ $ sudo ufw enable

Teclea "y" de yes y presiona Enter para que continúe. Si quisieras desactivar el modo interactivo del cortafuegos y que se ejecutará sin que te pregunte nada añade --force (sudo ufw --force enable)

Tambíen podemos añadir:

alice@raspberrypi:~ $ sudo ufw limit ssh/tcp 

que va a hacer que se niegue la conexión a una ip que ha tratado de conectarse 6 o más veces en los últimos 30 segudos.

Para terminar, puedes ver que funciona y que el protocolo ssh esta permitido tecleando:

alice@raspberrypi:~ $ sudo ufw status

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)

Si por la razón que fuese quisieras desactivar el cortafuegos utiliza el comando "disable".

Como el cortafuegos actualmente esta bloqueando todas las conexiones excepto ssh, si instalas y configuras servicios adicionales, necesitaras ajustar la configuración del mismo.

Puedes encontrar una guía básica de configuración en inglés aquí.

En resumen:

$ sudo apt-get install -y ufw
$ sudo ufw allow OpenSSH
$ sudo ufw --force enable
$ sudo ufw limit ssh/tcp 
# niega la conexion si una ip ha intentado conectarse 6 o más
# veces en los últimos 30 segundos.
$ sudo ufw status


8) Protegerse de ataques por fuerza bruta con Fail2ban

Para terminar con el apartado de "seguridad" vamos a ver como funciona muy resumidamente Fail2bin.

De la wikipedia: 

Fail2ban es una aplicación escrita en Python para la prevención de intrusos en un sistema, que actúa penalizando o bloqueando las conexiones remotas que intentan accesos por fuerza bruta

Lo primero como siempre es instalar el programa:

sudo apt install fail2ban

Básicamente vamos a instalar el programa con las opciones por defecto, pero añadiendo unas reglas básicas para ssh. Como vamos a usar unos parámetros diferentes crearemos el archivo /etc/fail2ban/jail.local para este propósito e introduciremos ahí los parámetros deseados. Estos sobrescriben los valores respectivos de jail.conf que no se deben tocar.

Teclearemos:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Una vez en el editor añadimos la siguiente regla:

[ssh]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 3
bantime = -1

Guardamos y salimos.

Nota:

maxretry son los intentos máximos de conexión erróneos que permitimos. En nuestro caso 3. OJO si te conectas a la pi por ssh mediante contraseña que si te confundes tres veces serás baneado y si encima tienes bantime con un número negativo, como en nuestro ejemplo, lo hace para siempre

Podemos quitar bantime y el programa usará el tiempo por baneo por defecto del programa o si queremos podemos poner un tiempo determinado por nosotros en segundos. Por ejemplo un baneo de una hora sería bantime=60000 o si queremos una semana también podemos poner bantime=1w

Si hacemos un cambio en el archivo de configuración, o bien reiniciamos el sistema o bien usamos:

sudo service fail2ban restart

Podemos ver la actividad de fail2ban con:

sudo fail2ban-client status ssh

o editando el siguiente archivo:

cat /var/log/fail2ban.log


D) IP Pública Dinámica y obtención de nombre de Dominio apuntando a nuestro servidor.

Otro requisito previo es que necesitamos acceder a la Raspsberry Pi desde fuera de nuestra red local (DNS DINAMICA) y tenemos que obtener un nombre de dominio que apunte a nuestro servidor. Ambas cosas las conseguiremos con lo siguiente.

Hasta ahora todo lo que hemos realizado es para acceder a la aplicación desde nuestra red local, para que nuestra Pi tenga siempre una dirección fija en nuestra red. Pero para acceder desde fuera de ella, el primer problema que se nos plantea es que normalmente solemos tener una dirección IP Pública variable, que no siempre es la misma, que puede cambiar. Es como si fuéramos a visitar a un amigo en el extranjero que hace tiempo que no vemos y cada vez que vamos a verle nos cambian el nombre y número de la calle.

Puedes saber cual es tu ip pública en un momento concreto buscando en cualquier buscador ¿Cuál es mi ip?

Para solventar esto y que aunque cambie nuestra ip pública el que busque nuestra página siempre sea redirigido a nuestra raspberry vamos a utilizar un servicio gratuito de una página muy conocida llamada https://www.noip.com/, aunque existen otras muchas (Freenom, Ducks Dns, DNS Exit, dynu, freedns.afraid.org etc)

La filosofía a groso modo es la siguiente. Instalaremos un programa en nuestra Rapsberry que le dirá a este proveedor cada poco tiempo (pej 5 minutos) cual es nuestra ip pública. Este proveedor nos facilita de forma gratuita hasta 3 dominios. Cuando nosotros, o el que sea los visite, será redirigido a nuestra ip pública actual automáticamente. Así ya no nos importa que esta haya cambiado, porque el dominio siempre hará referencia a nuestra ip actual.

Lo primero de todo es que en tu red local la Rapsberry Pi tenga asignada una dirección Ip Estática, que sea siempre la misma, tal como se supone que hiciste si seguiste esta guía, esta concretamente en el paso 1.

Luego nos registraremos en la página de NO-IP y crearemos una cuenta (Sign up). 

En el menú de la izquierda, seleccionamos donde pone Dynamic DNS y creamos el hostname que queramos.

 Para este proyecto imagina que escogimos miproyecto.hopeto.org (este dominio es ficticio, lo uso solo a modo de ejemplo, tu puedes escoger el quieras que este disponible)


Clicamos en donde pone Dynamic Update Cliente y seleccionamos el sistema operativo donde correrá el cliente de actualización de nuestra IP. En mi caso como estoy desplegando el proyecto en una Rapsberry pi 4 escogeré la opción de Linux. Hacemos click derecho en la pestaña "Download DUC" y seleccionamos la opción "Copiar dirección de enlace".


Volvemos al terminal y teleamos wget y le damos a pegar para que nos pegue la dirección del enlace de la descarga.

A la fecha de este tutorial la instrucción sería:

pi@raspberrypi:~ $ wget https://www.noip.com/client/linux/noip-duc-linux.tar.gz

Nos habrá descargado el archivo noip-duc-linux.tar.gz. Ya solo nos queda seguir las instrucciones de instalación.

- Extraemos el archivo comprimido con:

pi@raspberrypi:~ $ tar -zxvf noip-duc-linux.tar.gz 

Ahora tenemos un directorio llamado noip-2.1.9-1. Entramos dentro con:

pi@raspberrypi:~ $ cd noip-2.1.9-1/

Una vez dentro procedemos a instalarlo con:

pi@raspberrypi:~/noip-2.1.9-1 $ sudo make

pi@raspberrypi:~/noip-2.1.9-1 $ sudo make install


Hecho esto, el instalador nos preguntará el usuario con el que nos hemos registrado y la contraseña.

También nos preguntará con que intervalo actualizar nuestra Ip. Por defecto te aparece 30 minutos pero es más practico ponerlo en 5. Con esto ya tenemos el archivo de configuración creado.


Lo próximo es configurar el servicio del cliente de NO-IP para que arranque automáticamente cuando lo haga la raspberry. Para ello creamos el siguiente archivo:

pi@raspberrypi:~ $ sudo nano /etc/init.d/noip2 

Dentro ponemos la siguiente instrucción:

sudo /usr/local/bin/noip2

Guardamos y cerramos el archivo.

Ahora añadimos permisos de ejecución con:

pi@raspberrypi:~ $ sudo chmod +x /etc/init.d/noip2

Modificamos el fichero update-rc con:

pi@raspberrypi:~ $ sudo update-rc.d noip2 defaults

Y para finalizar lo ponemos en marcha con:

pi@raspberrypi:~/noip-2.1.9-1 $ sudo /usr/local/bin/noip2

Ya tenemos configurado el cliente Dinamic DNS Free de NO-IP y hemos obtenido, de forma gratuita, un dominio que apunta a nuetro servidor, que es nuestra Rapsberry Pi. (para este ejemplo miproyecto.hopeto.org)

Ahora simplemente hemos de abrir los puertos necesarios en el router (el puerto que hemos configurado en el hostname que hace una redirección Web. Normalmente abriremos el puerto 80 en el router), y al teclear en un navegador nuestro nombre de dominio, accederemos a nuestra Raspberry y a los servicios activados a través de ese puerto. Este paso lo haremos casi final del capítulo.
 


Paso 2: Crear un entorno virtual en Python e instalar la aplicación.

Aunque el paquete básico de Python ya esta instalado en Raspbian Pi Os, puede que algunos paquetes adicionales no lo estén o no estén actualizados para lo cual vamos a empezar con ello:

$ sudo apt-get -y update
$ sudo apt-get -y install python3 python3-venv python3-dev

A continuación crearemos un entorno virtual donde pondremos un programa Flask que nos servirá de muestra. En Rapsberry Pi OS lo necesario para crear un entorno virtual ya está instalado por defecto, pero si no lo tuvieras lo puedes hacer con:

sudo apt install python3-venv

A continuación creamos el directorio que contendrá la aplicación, yo lo llamaré "miproyecto".

$ mkdir ~/miproyecto

y entramos en el:

$ cd ~/miproyecto

Vamos a utilizar git para descargar y usar el código del capítulo anterior desde mi repositorio de GitHub. El problema es que no podemos usar directamente git clone https://github.com/chema-hg/CURSO-DE-FLASK.git porque nos descargaría todas las carpetas de todos los capítulos que ya hemos visto a parte del que nos interesa.

Por ello para clonar solamente el directorio que nos interesa, que es el 'POST 19', vamos a seguir los siguientes pasos:

- Inicializamos el proyecto con:

~/miproyecto $ git init

- Procedemos a indicar el repositorio remoto en donde se encuentra el proyecto:

~/miproyecto $ git remote add origin https://github.com/chema-hg/CURSO-DE-FLASK.git
- Habilitamos la configuración sparse-checkout con:

~/miproyecto $ git config core.sparsecheckout true

- Para clonar solamente un subdirectorio necesitamos definir que archivo o carpeta es la que queremos clonar dentro de la estructura del directorio (tenemos que teclear su nombre exactamente), lo cual hacemos de la siguiente manera:

~/miproyecto $ echo /POST 19/ >> .git/info/sparse-checkout
- Terminamos haciendo un pull al repositorio:

~/miproyecto $ git pull --depth=1 origin master
Si todo va bien tendrás dentro de la carpeta "miproyecto" otra llamada "POST 19". Para evitar confusiones voy a copiar todos los archivos y carpetas de "POST 19" a la carpeta "miproyecto" y luego borrare este directorio. 

~/miproyecto $ cd 'POST 19'/
~/miproyecto/POST 19 $ mv ~/miproyecto/'POST 19'/*  ~/miproyecto/
~/miproyecto/POST 19 $ cd ..
~/miproyecto $ rmdir 'POST 19'/

Bien ahora que ya tenemos todos los archivos y capetas necesarios creamos nuestro entorno virtual, para lo cual tecleamos:

pi@raspberrypi:~/miproyecto $ python3 -m venv miEntorno


La vista del directorio debe ser la siguiente:



Antes de instalar las aplicaciones necesarias en el entorno virtual tenemos que activarlo. Esto se consigue con el siguiente comando:

pi@raspberrypi:~/miproyecto $ source miEntorno/bin/activate

Verás algo como esto:

(miEntorno) pi@raspberrypi:~/miproyecto $ 

Lo que indica que ya estamos dentro del entorno virtual. El usuario que tengo para todo el proyecto es el que viene por defecto, el usuario "pi".


Paso 3: Configurar una aplicación de Flask.

Ahora que estamos dentro del entorno virtual es el momento de instalar Flask, todos los paquetes requeridos y Gunicorn y empezar a desarrollar la aplicación.

Nota: Independientemente de la versión de Python que uses, cuando uses el entorno virtual deberás usar el comando pip (no pip3)

Pero antes de empezar, tenemos que configurar nuestra aplicación de Flask para que funcione en un entorno de producción. Para ello lo primero de todo es instalar los paquetes requeridos por la aplicación. Dentro de nuestro entorno virtual escribimos:

~/miproyecto $ pip install -r requirements.txt

Lo que hará que se instalen todos los paquetes necesarios que hemos usado previamente para que el programa funcione que están en el archivo requirements.txt de nuestro directorio. (en el capitulo correspondiente los habíamos guardado usando $ pip freeze > requirements.txt)

IMPORTANTE: Ten en cuenta que ya estamos en un proceso de producción con lo que tendremos que desactivar el debug de flask ya que sino lo hiciéramos estaríamos permitiendo el ejecutar código arbitrariamente desde el navegador, lo cual estarás conmigo en que no es una buena práctica.

Tendremos que modificar el archivo config.py, en el cual están las configuraciones de nuestro programa. Entramos en el directorio "config" y allí editamos el archivo config.py del siguiente modo:

# depurador off para modo producción o eliminamos la línea ya que por defecto 
# el depurador esta desactivado.
DEBUG = False


Creación de un punto de entrada de WSGI

Cuando ejecutamos una aplicación de Flask estamos utilizando el servidor web que viene con Flask. Este servidor, que nos viene de maravilla mientras elaboramos la aplicación, no es una buena opción como servidor de producción ya que no se creo para ese fin, en cuando a rapidez y solidez del mismo. Para salir a la web se suele utilizar Gunicorn, que también es un servidor web escrito en Python, pero que a diferencia del de Flask, es un servidor de producción robusto que es utilizado por mucha gente y al mismo tiempo es muy fácil de usar.

Instalamos Gunicorn (porque flask ya estaba instalado con los requirements)

(miEntorno) pi@raspberrypi:~/miproyecto $ pip install gunicorn

Ahora, crearemos también un archivo que servirá como punto de entrada para la aplicación. Este archivo le va a indicar a nuestro servidor Gunicorn como interactuar con la aplicación.

Podemos llamar al archivo como queramos. Yo lo llamara wsgi.py.

(miEntorno) pi@raspberrypi:~/miproyecto $ sudo nano wsgi.py

En él, importamos la instancia de Flask desde nuestra aplicación y luego la ejecutaremos:


~/miproyecto/wsgi.py
from inicio import app

if __name__ == "__main__":
    app.run()

Guarda y cierra el archivo.

La estructura del directorio tiene que ser la siguiente:

miproyecto

|_ inicio.py

|_ wsgi.py

|_ miEntorno/


Paso 4: Configurar Gunicorn

Antes de continuar tenemos que comprobar que Gunicorn puede proveer correctamente la aplicación.

Podemos hacerlo pasándole el nombre de nuestro punto de entrada. Se hace con el nombre del archivo que hace de punto de entrada a la aplicación (wsgi menos la extensión py) más el nombre del elemento invocable dentro de la aplicación principal (app en nuestro caso).

También especificaremos la interfaz y el puerto al que se vinculará para que la aplicación se inicie de forma que este disponible públicamente. 

Como al principio instalamos el firewall y ahora ya esta activado, tenemos que abrir el puerto 8000 para que funcione la aplicación ya que es el que voy a utilizar para comunicarme con gunicorn y con la pi desde mi ordenador.

Lo haremos con la instrucción:

(miEntorno) pi@raspberrypi:~/miproyecto $ sudo ufw allow 8000

Ahora activamos el servidor con:

(miEntorno) pi@raspberrypi:~/miproyecto $ gunicorn --bind 0.0.0.0:8000 wsgi:app

o también mejor con:

(miEntorno) pi@raspberrypi:~/miproyecto $ gunicorn -b 0.0.0.0:8000 -w 4 wsgi:app

Deberías ver algo como esto:

[2021-04-28 10:14:09 +0200] [5866] [INFO] Starting gunicorn 20.1.0
[2021-04-28 10:14:09 +0200] [5866] [INFO] Listening at: http://0.0.0.0:8000 (5866)
[2021-04-28 10:14:09 +0200] [5866] [INFO] Using worker: sync
[2021-04-28 10:14:09 +0200] [5869] [INFO] Booting worker with pid: 5869
[2021-04-28 10:14:09 +0200] [5870] [INFO] Booting worker with pid: 5870
[2021-04-28 10:14:09 +0200] [5871] [INFO] Booting worker with pid: 5871
[2021-04-28 10:14:09 +0200] [5872] [INFO] Booting worker with pid: 5872

Visita  la dirección IP de tu servidor con :8000 al final en tu navegador web.

En mi caso:
http://192.168.1.200:8000

Deberías ver la página de entrada:



Cuando veas que funciona correctamente, pulsa Crtl-C en la ventana del terminal para cerrar la aplicación.

Expliquemos un poco el comando.

La Opción -b le dice a gunicorn donde escuchar las solicitudes. Le he dicho que escuche cualquier petición que se le haga en el puerto 8000. Por lo general, sería una buena idea ejecutar aplicaciones web de Python solo con acceso local y luego tener un servidor web muy rápido que esté optimizado para servir archivos estáticos aceptando todas las solicitudes de los clientes. Este servidor web si que servirá los archivos estáticos directamente aceptando todas las solicitudes de los clientes. En este ejemplo para ver que todo funciona correctamente la aplicación esta configurada con "0.0.0.0" aunque más adelante la dejaremos como localhost.

La opción -w configura cuantos usuarios de forma simultánea atenderá gunicorn. Tenerla configurada en 4 permite que la aplicación maneje hasta cuatro clientes simultáneamente, lo que para una aplicación web suele ser suficiente para manejar una cantidad decente de clientes, ya que no todos los que estén conectados (que pueden ser más) están pidiendo contenido constantemente. Dependiendo de la cantidad de ram de tu Raspberry Pi es posible que tengas que ajustar este número para que no te quedes sin memoria.

El argumento wsgi:app le dice a gunicorn de donde cargar la aplicación. 

De ahora en adelante ya no necesitaremos más el entorno virtual con lo que podemos desactivarlo.

(miEntorno) pi@raspberrypi:~/miproyecto $ deactivate

En adelante todos los comandos de Python usarán de nuevo el entorno de Python del sistema.


Paso 5: Ejecutar automaticamente Gunicorn.

Si bien como has visto Gunicorn es muy fácil de configurar, ejecutarlo manualmente desde una línea de comandos no parece una buena solución para un servidor en producción. Lo que necesitamos es tener el servidor ejecutándose en segundo plano y que se ejecute cada vez que se reinicie el sistema. 

A continuación voy a explicar dos opciones de hacerlo. Ambas son excluyentes es decir o utilizas una u la otra.

La primera me parece mucho más sencilla y funcional que la segunda porque nos aporta más ventajas. Pero por ver formas distintas de hacer lo mismo pondré las dos.


1) Opción.

Supervisor.

Lo último que hemos hecho es instalar Gunicorn y ver que funciona desde el terminal. Vamos ahora a hacer que todo lo anterior este monitorizado constantemente, porque si por alguna razón el servidor falla y se cierra, quiero asegúrame de que un nuevo servidor se ejecute en su lugar. Y también me quiero asegurar de que si la Raspberry Pi se reinicia, el servidor se ejecuta automáticamente al iniciarse, sin que tenga que iniciar sesión y teclear todos los comandos yo mismo.

Esto se consigue utilizando "Supervisor". Como dice en su documentación este programa es un sistema cliente servidor que permite monitorizar y controlar procesos en sistemas basados en Unix. 

Para empezar instalémoslo.

$ sudo apt-get -y install supervisor

En el archivo de configuración que utiliza supervisor tenemos que indicarle que programas debe monitorizar y como reiniciarlos cuando sea necesario. Los archivos de configuración deben guardarse en /etc/supervisor/conf.d. Abajo te dejo el archivo de configuración que utilizaremos para nuestro programa, al que llamaré miproyecto.conf


/etc/supervisor/conf.d/miproyecto.conf: Configuración de Supervisor.

[program:miproyecto]
command=/home/pi/miproyecto/miEntorno/bin/gunicorn -b localhost:8000 -w 4
directory=/home/pi/miproyecto
user=pi
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
Los parámetros command, directory y user le dicen a supervisor como debe ejecutar la aplicación. Autostart y autorestart sirven para que el servidor se reinicie automáticamente si la Raspberry se reinicia o se bloquea. Los parámetros stopasgroup y killasgroup garantizan que cuando "supervisor" necesite detener la aplicación para reiniciarla, tambien llegue a los procesos secundarios de gunicorn de nivel superior.

Después de que escribas y guardes el archivo de configuración, debes volver a cargar el servicio de supervisor para que se importe.

$ sudo supervisorctl reload

Y ya está, ¡el servicio web que nos proporciona gunicorn debería estar funcionando y estar supervisado!


2) Opción.

Crea un archivo de unidad de servicio systemd

Un archivo en systemd te permitirá que al iniciar la Rapsberry Pi automáticamente se ejecute Gunicorn y sirva nuestra aplicación Flask siempre que se inicie el servidor. (pero OJO, no la monitoriza como en la opción anterior)

Empezaremos por crear un archivo con extensión .service dentro del directorio /etc/systemd/system. Para que todo sea acorde al proyecto se llamará miproyecto.service

$ sudo nano /etc/systemd/system/miproyecto.service

El archivo de forma genérica debe tener el siguiente contenido:

[Unit]#  especifica metadatos y dependenciasDescription=Instancia de Gunicorn para mi proyecto
After=network.target
# Le decimos al sistema de inicio que comience tras haberse conectado a una red.[Service]# Especificamos el usuario y el grupo con los cuales deseamos que se ejecute el proceso. # Le damos a nuestra cuenta habitual de usuario la propiedad del proceso, ya que posee todos los archivos relevantes.
User=tu_nombre_de_usuario
# También otorgamos la propiedad del grupo al grupo www-data para que posteriormente Nginx (nuestro servidor) pueda comunicarse fácilmente con los procesos de Gunicorn.Group=www-data# A continuación especificamos los detalles del directorio de trabajo y estableceremos las variables de entorno en el PATH para que el sistema Init sepa que los ejecutables para el proceso, o sea nuestra aplicación, están ubicados dentro de nuestro entorno virtual.WorkingDirectory=/home/nombre_usuario/directorio_proyecto
Environment="PATH=/home/nombre_usuario/directorio_proyecto/entorno_virtual/bin"
Luego le decimos cual es el comando para iniciar el servicioExecStart=/home/nombre_usuario/directorio_proyecto/entorno_virtual/bin/gunicorn --workers 4 --bind unix:miproyecto.sock -m 007 wsgi:app# Este comando hará lo siguiente:# Le decimos que inicie 4 procesos workers (esto se podría ajustar si es necesario)# Además especificamos una mascara 007 para que se cree el archivo de socket, se proporcione acceso al propietario y al mismo tiempo se restrinjan otros accesos.# Le especificamos el nombre del archivo del punto de acceso a WSGI junto con el elemento invocable de Python dentro de ese archivo (wsgi:app)# Si justo al final de toda la instrucción le añadimos el símbolo & ejecutaríamos el servidor en segundo plano.# Esto indicará a systemd a qué deberá vincular este servicio si lo habilitamos para que se cargue en el inicio. Queremos que este servicio se inicie cuando el sistema multiusuario normal esté en funcionamiento:[Install]
WantedBy=multi-user.target


En la práctica lo que debemos copiar a nuestro archivo miproyecto.service es:

/etc/systemd/system/miproyecto.service

[Unit]
Description=Instancia de Gunicorn para mi proyecto
After=network.target

[Service]
User=pi
Group=www-data
WorkingDirectory=/home/pi/miproyecto
Environment="PATH=/home/pi/miproyecto/miEntorno/bin"
ExecStart=/home/pi/miproyecto/miEntorno/bin/gunicorn --workers 4 --bind unix:miproyecto.sock -m 007 wsgi:app

[Install]
WantedBy=multi-user.target


Guardamos y cerramos el archivo.

Con esto ya lo tenemos completo y podemos iniciar el servicio Gunicorn y activarlo para que se cargue al iniciar el sistema. 

  • sudo systemctl start miproyecto
  • sudo systemctl enable miproyecto

Y si comprobamos su estado con el siguiente comando:

sudo systemctl status miproyecto

Deberías ver el siguiente resultado:








Control + C para finalizar el proceso.

Además un nuevo archivo llamado miproyecto.sock se habrá creado en el directorio de nuestro proyecto automáticamente.

La estructura del directorio quedaría como sigue:

miproyecto
|____ miproyecto.py
|____ wsgi.py
|____ miEntorno
|____ miproyecto.sock


En este punto si detectas algún error es mejor que intentes resolverlos antes de continuar con el resto del tutorial.


Paso 6: Instalar y configurar Nginx para solicitudes de proxy.

Ahora que ya tenemos Gunicorn funcionando tenemos que configurar e instalar Nginx para que trasmita las peticiones web al socket. Si has optado anteriormente por la opción 1 el servidor de mi proyecto que está utilizando gunicorn se esta ejecutando de forma privativa en el puerto 8000. Si por el contrario has utilizado la opción 2 estará utilizando miproyecto.sock. 

Lo que necesitamos hacer ahora para conectar la Pi al mundo exterior es instalar Nginx, abrir los puertos del router en las direcciones 80 y 443, que son los puertos que vamos a abrir en el firewall para manejar el tráfico web de la aplicación.

Primeramente instalamos Nginx

pi@raspberrypi:~/miproyecto $ sudo apt-get install nginx

Antes de probar Nginx debemos saber que este programa, en relación con el cortafuegos, se registra a si mismo como un servicio con ufw en el momento de la instalación lo que permite el acceso al servicio. 

Vemos como Nginx está en la lista de las aplicaciones con las que ufw sabe trabajar en conjunto.

Teclea:

pi@raspberrypi:~ $ sudo ufw app list

Entre otras verás las siguientes aplicaciones:

Available applications:
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH

Como puedes ver hay tres perfiles disponibles para Nginx.

Nginx Full - Este perfil abre el puerto 80 (tráfico web normal sin cifrar) y el puerto 443 (tráfico cifrado TLS/SSL)

Nginx HTTP - Este perfil solo abre el puerto 80 (el del trafico normal web sin cifrar)

Nginx HTTPS - Este perfil solo abre el puerto 443 (tráfico cifrado TLS/SSL)

Luego, permitimos el acceso total al servidor de Nginx:

  • $ sudo ufw allow 'Nginx Full' o también (que es lo que he usado yo) podemos abrir los puertos directamente con estas 2 instrucciones en vez de la anterior : $ sudo ufw allow http $ sudo ufw allow 443/tcp

Con esto permitiremos el tráfico en los puertos 80 (http) y 443 (https)

Como queremos que nuestro proyecto tenga una implementación segura, configuraremos el puerto 80 para que reenvíe todo el tráfico que le llegue al puerto 443 (https) que se cifrará. Lo primero que tenemos que hacer es crear un certificado SSL. Por ahora vamos a crear un certificado SSL autofirmado, de andar por casa, que está bien para que probemos que todo funciona, pero que no es bueno para una implementación real ya que los navegadores web te darán mensajes de advertencia, porque el certificado no fue emitido por una autoridad certificadora confiable.

El comando para crear el certificado SSL es el siguiente. Sitúate dentro del directorio raíz miproyecto, si no lo estás y teclea:

/home/pi/miproyecto

$ mkdir certs
$ openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \
  -keyout certs/key.pem -out certs/cert.pem
Cuando lo ejecutes te pedirá una seria de preguntas que te puedes saltar sobre tu nombre y organización que son las que se mostrarán si los usuarios solicitan ver el certificado en los navegadores web.

Además se crearán dos archivos dentro del directorio que hemos creado llamados key.pen y cert.pen 

Para tener un sitio web servido por nginx, este archivo debe estar en el directorio /etc/nginx/sites-enabled. Nginx, en su instalación, crea un sitio de prueba en esta dirección que no necesitamos asi que lo borraremos.

$ sudo rm /etc/nginx/sites-enabled/default


Ahora tenemos que enseñarle a Nginx donde esta nuestra app y como tiene que servirla.

En el directorio /etc/nginx/ es donde están localizados los archivos de configuración.

Los dos directorios con los que trabajaremos son sites-available y sites-enabled

- sites-available contiene los archivos de configuración individual de cada una de tus posibles aplicaciones o páginas web de carácter estático.

- sites-enabled contiene los enlaces a los archivos de configuración que Nginx leerá y ejecutará.

Vamos a crear un nuevo archivo de configuración en el directorio de los sitios disponibles de nginx llamado miproyecto, para que se adecue a la denominación de los archivos del resto del tutorial.

$ sudo nano /etc/nginx/sites-available/miproyecto

A continuación puedes ver el archivo de configuración de nginx que vamos a utilizar.


/etc/nginx/sites-available/miproyecto: Configuración de Nginx.

server {
    # listen on port 80 (http)
    listen 80;
    server_name _;
    location / {
        # redirect any requests to the same URL but on https
        return 301 https://$host$request_uri;
    }
}
server {
    # listen on port 443 (https)
    listen 443 ssl;
    server_name _;

    # location of the self-signed SSL certificate
    ssl_certificate /home/pi/miproyecto/certs/cert.pem;
    ssl_certificate_key /home/pi/miproyecto/certs/key.pem;

    # write access and error logs to /var/log
    access_log /var/log/miproyecto_access.log;
    error_log /var/log/miproyecto_error.log;

    location / {
        # forward application requests to the gunicorn server
        proxy_pass http://127.0.0.1:8000;
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /static {
        # handle static files directly, without forwarding to the application
        alias /home/pi/miproyecto/static;
        expires 30d;
    }
}

IMPORTANTE: Este archivo de configuración esta preparado para usarse con la opción 1 del punto anterior, si has usado la opción 2 es lo mismo pero tienes que sustituir lo rojo, el proxy pass por este:

proxy_pass http://unix:/home/pi/miproyecto/miproyecto.sock;

Guarda y cierra el archivo al finalizar.

Por si quieres indagar más sobre la configuración de Nginx te dejo este enlace de su documentación oficial. 

Para habilitar la configuración del bloque server que acabamos de crear, tenemos que vincular el archivo al directorio sites-enabled:

$ sudo ln -s /etc/nginx/sites-available/miproyecto /etc/nginx/sites-enabled

Podemos comprobar si hay errores tecleando:

pi@raspberrypi:~ $ sudo nginx -t


Si no hay ningún problema, reiniciamos el proceso Nginx para que tenga en cuenta la nueva configuración.

pi@raspberrypi:~/miproyecto $ sudo systemctl restart nginx

o

pi@raspberrypi:~/miproyecto $ sudo service nginx reload


Y finalmente ya deberíamos tener corriendo la aplicación. En tu navegador web puedes teclear la dirección IP de su servidor, (192.168.1.200 es la dirección de red de mi Pi) y se conectará al servidor. Como estamos utilizando un certificado autorfirmado verás una advertencia del navegador web



que deberás descartar para finalmente ver el resultado de todo el trabajo:

















Ya solo nos queda acceder desde fuera de nuestra red. Vamos con ello.

Nota: si usamos la opcion 1, ajustamos el firewall de nuevo. Ya no necesitamos el acceso a través del puerto 8000, por lo que podemos eliminar esta regla. 

  • sudo ufw delete allow 8000


- Apertura de Puertos.

Para que todo pueda funcionar correctamente necesitamos abrir el puerto 80 y el 433 en nuestro router. ¿Por que? Pues porque cuando tecleas el nombre de dominio en un navegador (pej miproyecto.hopeto.org) NO-IP redirige la petición a nuestra IP-publica. Pero claro, el router como debe de ser, no deja entrar a cualquiera a nuestro sistema. Por ello tenemos que decirle que las peticiones web que reciba, las derive a la dirección ip de la Pi en nuestra red interna (en el ejemplo 192.1.1.200) y a través del puerto 80 o el 443, ya lo veremos.

Cada router es un mundo, así que tendrás que ver como se hace en el tuyo. En el mío entras en la dirección del router, tecleas la contraseña de acceso y entras en una sección que pone "puertos". Allí creas una nueva regla, abriendo tanto para el puerto 80, como el 443 ya que posteriormente lo necesitaremos para proteger nuestra aplicación usando HTTPS.




Guardamos y cerramos.

Probamos nuevamente que todo funciona correctamente accediendo al nombre del dominio que escogiste en NO-IP (En mi caso miproyecto.hopeto.org) desde un equipo que no este conectado a la red local (por ejemplo desde el móvil sin que este conectado a la red local claro) y que apunta a nuestro servidor, que es la Rapsberry, desde nuestro navegador.





Paso 7: Proteger la aplicación.



Para que la navegación hacia nuestro servidor sea seguro necesitamos obtener un certificado SSL para nuestro dominio. Vamos a utilizar la forma más sencilla y gratuita que es obtener un certificado gratuito de Let´s encrypt. Según la web de su página oficial let´s encrypt es una autoridad certificadora gratuita, automatizada y abierta, destinada para el beneficio público ofrecido por el Internet Security Research Group. Nos facilitan certificados digitales para habilitar https completamente gratuitos.

Los certificados tienen una vigencia de tres meses aunque disponemos de un sencillo comando en el crontab del sistema para automatizarlo.

Hasta ahora nuestro servidor, si recibe una petición HTTP para comunicarse (con lo que los datos viajan en internet tal cual y son accesibles para cualquiera que intercepte la comunicación), lo que hace es redirigirla a través del protocolo HTTPS, con lo que conseguiremos asegurar nuestra aplicación ya que los datos viajan de un modo seguro de un lado al otro ya que están encriptados mediante un cifrado SSL

También hasta ahora, los certificados SSL los habíamos firmado nosotros mismos. Vamos a ver como hacer para usar certificados "reales".

Cuando solicitamos un certificado a una autoridad certificadora, esta va a verificar que nosotros somos los que tenemos el control tanto del servidor como del dominio, pero el como se pasa esta verificación depende de cada entidad. Si el servidor pasa esta verificación, la entidad nos emitirá un certificado con su propia firma y nos lo dará para que lo instalemos. El certificado será valido por un periodo de tiempo que generalmente no excede del año. La mayoría de la entidades certificadoras cobran por prestar este servicio, pero hay algunas que lo ofrecen de forma gratuita. La más popular se llama Let´s Encryp.

Obtener un certificado de Let´s Encryp es muy sencillo ya que todo el proceso ser realiza de forma automática. 

Empezaremos instalando el paquete de Nginx para Certbot:

pi@raspberrypi:~ $ sudo apt install python-certbot-nginx

Podemos comprobar si está correctamente instalado y su versión con:

pi@raspberrypi:~ $ certbot --version

Como vamos a pedir los certificados para miproyecto.hopeto.org tenemos que hacer 2 pequeñas modificaciones en el archivo:

sudo nano /etc/nginx/sites-available/miproyecto

server {
    # listen on port 80 (http)
    listen 80;
    server_name miproyecto.hopeto.org;
    location / {
        # redirect any requests to the same URL but on https
        return 301 https://$host$request_uri;
    }
}
server {
    # listen on port 443 (https)
    listen 443 ssl;
    server_name miproyecto.hopeto.org;

    # location of the self-signed SSL certificate
    ssl_certificate /home/pi/miproyecto/certs/cert.pem;
    ssl_certificate_key /home/pi/miproyecto/certs/key.pem;

    # write access and error logs to /var/log
    access_log /var/log/miproyecto_access.log;
    error_log /var/log/miproyecto_error.log;

    location / {
        # forward application requests to the gunicorn server
        proxy_pass http://127.0.0.1:8000;
        # o proxy_pass http://unix:/home/pi/miproyecto/miproyecto.sock;
        # si usas la opcion 2
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /static {
        # handle static files directly, without forwarding to the application
        alias /home/pi/miproyecto/static;
        expires 30d;
    }
}


Ahora configuramos el entorno para que sea usado con Nginx. Podemos hacerlo de dos maneras. La primera sería usando la instrucción $ sudo certbot --nginx o indicando el nombre de dominio que tenemos directamente en la línea de comandos. Yo usaré esta última:

pi@raspberrypi:~ $ sudo certbot --nginx -d miproyecto.hopeto.org

nota: sustituye miproyecto.hopeto.org por el nombre de tu dominio.

En el proceso de configuración lo primero que te pide es que introduzcas un correo con el que puedan contactar contigo. Luego que aceptes las condiciones del servicio (A), si quieres recibir información o no en ese correo electrónico y a final nos dan dos opciones que son las que nos interesan:

Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
-------------------------------------------------------------------------------
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel):

1) No redirigir. Es decir puedes tener los dos protocolos de manera independiente. Es decir vas a poder acceder tecleando en el navegador http://miproyecto.hopto.org o https://miproyecto.hopto.org

2) Redirigidas. Es decir que las peticiones que se realicen a través de http://miproyecto.hopto.org sean automáticamente redirigidas al protocolo HTTPS.

Yo escogeré está opción (la número 1)

Si te acuerdas, cuando hicimos nuestras claves autofirmadas, especificamos donde estaban en el archivo  /etc/nginx/sites-available/miproyecto dentro de las claves:

 # location of the self-signed SSL certificate
    ssl_certificate /home/pi/miproyecto/certs/cert.pem;
    ssl_certificate_key /home/pi/miproyecto/certs/key.pem;
Al terminar de ejecutar certbot nos saldrá donde ha guardado let´s encrypt los certificados:

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/miproyecto.hopeto.org/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/myproyecto.hopeto.org/privkey.pem

Aunque deberíamos cambiar a mano nuestros certificados autofirmados por estos últimos, estamos de enhorabuena porque el programa de instalación de certbot ya lo ha hecho por nosotros.

/etc/nginx/sites-available/miproyecto: Nginx configuracion de claves let´s encrypt.

...
# location of the self-signed SSL certificate
    ssl_certificate /etc/letsencrypt/live/miproyecto.hopto.org/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/miproyecto.hopto.org/privkey.pem; # managed by Certbot
...

Si ahora visitas la página veras como arriba a la izquierda en la barra de direcciones te aparece el candado. ¡Éxito!


Renovación de certificados. 

Como los certificados tienen una vigencia de tres meses podemos hacer la renovación de los mismos de forma manual o automática.

Para comprobar los certificados de nuestro sistema podemos usar el comando:

pi@raspberrypi:~ $ sudo certbot certificates

La salida de este comando es información sobre el certificado entre la cual está la fecha en la que expira.

La renovación podemos efectuarla:

De forma manual para todos los certificados del sistema.

Si estamos a menos de una semana para que expire el certificado, este estará disponible para renovar, y podremos hacerlo con el siguiente comando:
sudo certbot renew
Si estamos fuera de ese plazo no hará nada.


De forma automática.

Si queremos que la renovación se realice de forma automática podemos programar el servidor para que realice esta tarea a través del crontab del propio sistema.

Por ejemplo que todos los días se intente la renovación a las 3 y 15 de la madrugada.

$ crontab -e
15 3 * * * /usr/bin/certbot renew --renew-hook "service nginx reload"
Como el proyecto ya está finalizado y en funcionamiento si queremos ver el grado de seguridad de nuestro servidor podemos visitar el sitio de Qualys SSL LABS donde podemos obtener un informe sobre la seguridad de nuestro servidor. Lo más probable es que nos diga que nos queden algunas cosas menores por pulir. Por ejemplo en mis caso me daba una nota de "B" porque desde 31-01-2020 a los servidores que soportan los protocolos TSL 1.0 o TSL 1.1 automáticamente se les asigna la nota "B" y además por defecto la instalación de nginx no tiene activado el protocolo TSL 1.3.

Para arreglar esto vas a el archivo de configuración de nginx que está en /etc/nginx/nginx.conf quitamos del parámetro ssl_protocols las versiones TSL 1 y añadimos TSL 1.3 dejándolo de la siguiente forma:
(También lo podemos poner en el archivo de configuración de nuestro proyecto paa nginx /etc/nginx/sites-available/miproyecto)
ssl_protocols TLSv1.2 TLSv1.3;
Si ahora pasamos el test a nuestra página, casi está perfecta.


Tambien puedes comprobar y obtener más recomendaciones de seguridad de tu servidor en la página de mozilla observatory.

Otra de las áreas en que podemos mejorar es cómo se generan los coeficientes que se utilizan durante el intercambio de las claves de cifrado, que suelen tener unos valores predeterminados bastante débiles. En particular los coeficientes Diffie-Hellman tardan una cantidad considerable de tiempo en generarse, por lo que los servidores utilizan números más pequeños de forma predeterminada para ahorrar tiempo. Sin entrar en detalle, el intercambio de claves Diffie-Hellman tiene una propiedad interesante que es que aún en el caso de que un atacante obtuviese la clave privada de nuestro servidor, le sería muy difícil romper la comunicación entre nuestro servidor y sus clientes. Sin embargo el tamaño de la clave predeterminada en OpenSSL es de 1024 bits, con lo que en teoría con la ayuda de un supercomputador se podría romper. 

Por suerte podemos pre-generar coeficientes más fuertes y guardarlos en un archivo que luego Nginx podrá usar. Eso si, ármate de paciencia porque, si quieres como yo establecer el DH en 4096 bits vas a tener que esperar mucho tiempo ya que hace falta un uso intensivo de la CPU que la Raspberry no tiene. Para que te hagas una idea en mi raspberry pi 4 ha tardado 6  horas en generarse. 

Lo primero que haremos será generar nuestro coeficiente Diffie-Hellman de 4096 bits.

cd /etc/ssl/certs
sudo openssl dhparam -out dhparam.pem 4096

* Si quieres que tarde menos, usa 2048 en lugar de 4096 que también está muy bien.

Cuando haya terminado tendremos un archivo llamado dhparam.pem
Luego añadiremos o modificaremos la siguiente línea en el archivo de configuración de Nginx dentro de las opciones del server:

ssl_dhparam /etc/ssl/certs/dhparam.pem;

/etc/nginx/sites-available/miproyecto: archivo de configuración de Nginx.

server {
    listen 443 ssl;
    server_name miproyecto.hopeto.org;
    ....
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ...

Guardamos, salimos y reiniciamos el servidor.

Si ya queremos conseguir el A+ probablemente tendremos que especificar que tipo de cifrados permite nuestro servidor. La organización Mozilla nos facilita un generador de configuraciones SSL que introduciendo el tipo de servidor (nginx en nuestro caso), la versión del mismo  y la versión de OpenSSL nos facilita un estupendo archivo de configuración.

Nota: para saber la versión de nginx utiliza: $ nginx -v y para saber la versión de OpenSSL utiliza: $ openssl version

Esta es entrada que yo tengo a fecha de hoy en mi archivo de configuración:

/etc/nginx/sites-available/miproyecto: archivo de configuración de Nginx.

server {
    listen 443 ssl;
    server_name miproyecto.hopeto.org;
    ....
# intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

Y con esto ya he conseguido una nota de A+ en Qualys SLL LABS. 

No obstante si te interesa la seguridad puedes entrar en la página de Mozilla Observatory que te indique antes e ir corrigiendo todos los fallos que te diga que con seguridad serán unos cuantos. Puedes encontrar ayuda en esta página.