Publi

Monta microservicios web rápidamente en Python con web.py


Python es uno de los lenguajes de moda. En sus múltiples usos: para escritorio, aplicaciones científicas, web, scripting y mucho más. Algo que también está de moda son los microservicios. Grosso modo, un microservicio es un componente independiente que implementa una funcionalidad de nuestra aplicación. Será una pieza de un puzzle mayor que, dadas unas especificaciones, podremos mejorar, reescribir, cambiar de lenguaje, utilizar bases de datos diferentes, etc.

Y como ambas tecnologías están de moda, vamos a juntar lo mejor de los dos mundos y combinarlo. Porque gracias a versatilidad de Python, de la cantidad y calidad de muchas de las bibiliotecas que podemos utilizar, se convierte en un lenguaje con el que rápidamente podemos desarrollar proyectos mucho más grandes.

Diseño con microservicios versus diseño monolítico

Históricamente, las aplicaciones web se han diseñado de forma que funcionan como un todo. Tanto la gestión de usuarios, contenidos, cuentas, etc. Utiliza una base común, o un framework y todo funciona dentro del mismo proyecto de código. Normalmente el rendimiento de esto es muy bueno y no nos dará muchos problemas. Aunque cuando las aplicaciones web se van haciendo más y más grandes, y junto con ellas, los equipos de trabajo van apareciendo algunos problemas:

  • Todo el mundo termina trabajando en el mismo repositorio y todo el mundo ve todo el código de la aplicación. Y puede que no nos interese que un nuevo empleado de nuestra empresa lo pueda ver todo. O incluso que se toquen, o modifiquen, partes del código que no deben modificarse. Porque al final, muchos programadores toman atajos. O incluso alguien, tal vez quiera arruinar nuestro proyecto.
  • Cuando nuestro sistema recibe muchas visitas, debemos escalarlo para que éste pueda atender a más usuarios. Puede ser tan sencillo como introducir balanceo de carga, aunque eso nos obliga a tener que pasar todo por el balanceador. Puede que haya partes que no necesitemos balancear, o que necesiten menor replicación. O puede que haya partes más fáciles de replicar que otras.
  • En aplicaciones grandes, por razones de mantenimiento, deberíamos poder activar y desactivar funcionalidades simples, en lugar de poner la web entera en mantenimiento.
  • Se pueden dar casos en los que tengamos que interactuar con una aplicación de terceros. Pero no podamos o queramos modificarla. Por lo que podemos crear pequeños servicios para controlarla remotamente.
  • No estamos limitados a un lenguaje de servidor. Puede que parte de la aplicación esté hecha en C++, otra parte en PHP y otras partes en Perl, Python o Java.
  • Por seguridad, hemos de ser conscientes de que el sistema más seguro es el que no está conectado, o es menos accesible. Por eso mismo, deberíamos configurar un esquema de red en el que la información más sensible esté lo más separada posible del usuario final. Que el mismo servidor web no tiene acceso a los datos de usuarios directamente es una situación ideal. Hacer que éstos estén detrás de una red interna accesible sólo a través de un sistema intermedio nos puede dar algo más de confianza. Ya que los datos no tienen conexión directa. Al mismo tiempo que reducimos en cierto modo la capacidad de algún cyberdelincuente, reducimos la capacidad de acceso a datos de algunos empleados de nuestra empresa.

Para ello, podemos optar por un diseño basado en APIs web que se van conectando las unas con las otras. Algunas APIs serán públicas, tendrán acceso desde el exterior; otras, en cambio, serán privadas, y sólo serán accesibles dentro de la red local donde estén situadas.

Lo primero que podemos pensar es que este enfoque es lento. Que lo es. Es decir, no es lo mismo que nuestra aplicación monolítica llame a una función para que inserte un registro en una base de datos a que la aplicación llame a una función que haga una petición HTTP a otra máquina para que ésta inserte el registro en base de datos. Además, para hacer la petición HTTP, en el lado del cliente (el que pide) y el servidor (el que inserta el registro) debe haber un tratamiento y filtrado de datos que propicie el envío y recepción de datos. Al final, un proceso que tarda menos de 1ms se transforma en unos 2 o 3ms. Así que tenemos que tener claro los pros y los contras de nuestro caso concreto antes de optar por un diseño u otro. Y, aunque la velocidad de los servicios puede verse afectada cuando hay pocos usuarios, tal vez las posibilidades de escalado que nos brinda la nueva arquitectura pueda hacer que el sistema no se resienta demasiado cuando el número de usuarios crezca.

REST contra el mundo

Aunque un sistema REST no es que sea lo más rápido del mundo. Aunque podemos aprovecharnos de la madurez y longevidad de un montón de bibliotecas y sistemas que utilizan HTTP para comunicarse. Este tipo de comunicación está muy documentada y podemos crear rápidamente sistemas que se comuniquen de esta forma. Hay muchos sistemas que utilizan protocolos propios para realizar la comunicación y el intercambio de datos. Pero si lo que realmente queremos es diseñar un sistema en poco tiempo y que funcione bien deberíamos optar por sistemas con cierta madurez. Si diseñamos un protocolo desde cero tendremos que implementar el cliente y el servidor.

Podríamos utilizar otros protocolos basados en HTTP, como SOAP, aunque no será demasiado costoso implementarlo a partir del ejemplo de abajo.

Crear servicios con Python

Python es un lenguaje que podemos utilizar para casi cualquier cosa. Muchos programas de escritorio, móviles, incluso de IoT se han desarrollado en Python. Y el ecosistema web no iba a ser menos. Aunque tenemos varias opciones, he optado por utilizar web.py por su simplicidad y su documentación.

Primero vamos a instalar la biblioteca web.py con pip:

pip install web.py

¡Vamos al código! Quiero realizar un ejemplo sencillo, porque esto se puede complicar hasta decir basta. El ejemplo tendrá un listado de países con un código numérico, nombre y código ISO. El código se podría mejorar utilizando una base de datos en lugar de un diccionario, incluyendo el método PATCH, o creando una clase padre para unificar un poco el código, pero en esta ocasión quiero mostrar el funcionamiento básico del servicio REST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# coding=utf-8

import web
import json

urls = (
    '/paises(/.*)?', 'paises',
)

application = web.application(urls, globals()).wsgifunc()

class paises:
    paises = { 34: { 'España', "ES"}, 33: {"Francia", "FR"} }
    codes = { 400 : '400 Bad Request',
              404 : '404 Not Found',
              405 : '405 Method Not Allowed',
              409 : '409 Conflict'
              }
    def __init__(self):
        web.header('Content-Type', 'application/json', unique=True)

    def GET(self, pais=None):
        try:
            columns = [ 'codigo', 'nombre', 'iso' ]
        if pais is None:
                output = []
        for i,v in self.paises.items():
                    output.append(dict(zip(columns, [i] + list(v))))
        else:
                pais = int(pais[1:])
                if self.paises[pais] is None:
                    raise Exception('Pais no encontrado', 404)
                else:
                    output = []
                    output.append(dict(zip(columns, [pais] + list(self.paises[pais]))))

            return json.dumps(output, ensure_ascii=False, encoding='utf8')
        except Exception, e:
            msg, code = e.args if len(e.args)==2 else (e.args, 404)
            raise web.HTTPError(self.codes[code], data="Error: " + str(msg) + "\n")

    def POST(self, pais=None):
        try:
            if pais is not None:
                raise Exception('No permitido', 404)
           
            input = web.input(code=None, nombre=None, iso=None)
            if not input['code'] or not input['nombre'] or not input['iso']:
                raise Exception("Faltan datos de entrada", 400)

            pais = int(input['code'])
            if pais in self.paises:
                raise Exception("Elemento existente", 409)
           
            self.paises[pais] = { input['nombre'], input['iso'] }
            web.created()
            web.header('Location', '/paises/'+str(pais))
            return ''
        except Exception, e:
            msg, code = e.args if len(e.args)==2 else (e.args, 404)
            raise web.HTTPError(self.codes[code], data="Error: " + str(msg) + "\n")

    def PUT(self, pais=None):
        try:
        if pais is None or len(pais)==1:
                raise Exception('Pais no indicado', 405)
           
            pais = int(pais[1:])
            input = web.input(nombre=None, iso=None)
            if not input['nombre'] or not input['iso']:
                raise Exception("Faltan datos de entrada", 400)
           
            if pais not in self.paises:
                raise Exception("Elemento no encontrado", 404)
           
            self.paises[pais] = { input['nombre'], input['iso'] }
            return ''

        except Exception, e:
            msg, code = e.args if len(e.args)==2 else (e.args, 404)
            raise web.HTTPError(self.codes[code], data="Error: " + str(msg) + "\n")

    def DELETE(self, pais=None):
        try:
        if pais is None or len(pais)==1:
                raise Exception('Pais no indicado', 405)
           
            pais = int(pais[1:])          
            if pais not in self.paises:
                raise Exception("Elemento no encontrado", 404)
           
            del self.paises[pais]
            return ''

        except Exception, e:
            msg, code = e.args if len(e.args)==2 else (e.args, 404)
            raise web.HTTPError(self.codes[code], data="Error: " + str(msg) + "\n")

if __name__ == "__main__":
    app = web.application(urls, globals())
    app.run()

Probando nuestro código

Para probar el código basta con ejecutar la aplicación:

python webservice.py
http://0.0.0.0:8080/

Se creará un servidor web en nuestra máquina utilizando el puerto 8080 y podremos utilizar cualquier navegador o cURL para hacer pruebas.
Como punto interesante, destaco que podemos hacer modificaciones en nuestro código sin necesidad de volver a ejecutar el programa. Es decir, podemos modificar nuestros ficheros y volver a cargar las URLs sin problemas. Además, incluye un buen depurador de Python que nos indicará los errores en el código y nos ayudará a solventarlos.

Montando el servicio en servidor

Nginx + uWSGI
Si queremos montar un servidor para correr este servicio, recomiendo encarecidamente, utilizar un servidor web como Nginx o Apache en lugar de exponer al exterior el programa en Python. Ya que los servidores web colocarán una capa de filtrado y aislamiento entre el programa y el usuario final. Además, como son muy utilizados en producción, las versiones estables intentan tener todos los fallos corregidos.
No es raro que algún servidor de desarrollo, como puede ser este creado por web.py, no se entienda bien con algunos datos de entrada que no cumplan las normas y provoque un fallo en nuestro sistema. Mientras que un Apache, suele estar bien preparado (casi siempre) ante atacantes avispados.

En Apache, podríamos utiliza FastCGI o mod_wsgi y con Nginx podríamos utilizar un proxy HTTP o WSGI. Un punto positivo de la utilización de Python en un entorno web es que los scripts están cargados en memoria y ejecutándose todo el tiempo, por lo que podemos tener alguna información precargada o conexiones a base de datos abiertas sin necesidad de ejecutar todo desde cero a cada petición entrante. Aunque, para atender muchas peticiones entrantes, y a veces concurrentes, es necesario que dispongamos de varias ejecuciones del script. O, al menos, un programa que controle dichas ejecuciones, lance y destruya instancias de la aplicación. En mi caso, en algún servicio que he hecho he preferido utilizar uWSGI para gestionar las instancias de mi aplicación en Python.

UWSGI cuenta con un protocolo de comunicación tanto para Apache como Nginx que harán que estos servidores web hablen con uWSGI cuando entre una petición enviándole los datos de entorno y usuario. UWSGI verá si hay alguna instancia en ejecución de la aplicación que no esté atendiendo a nadie y le mandará los datos a ésta. Si no hay ninguna, intentará lanzar una nueva, dentro de los límites que establezcamos o esperará que haya una libre durante un tiempo para atender la petición. Cuando la aplicación haya generado una salida, uWSGI la recibirá y éste se la enviará al servidor web que ya es el que se peleará con el usuario. De todas formas el servidor web siempre tendrá la primera y la última palabra. Y con esto me refiero a que el servidor web debe contar con sus propios sistemas de seguridad y filtros que decidirán si pasar o no la petición a uWSGI, e incluso si son ficheros estáticos los pueden servir ellos mismos. Y una vez pasada la petición, antes de enviarla al usuario, podrán modificar o insertar cabeceras, incluso filtrar o comprimir el contenido generado.

proxy con protocolo uWSGI

UWSGI permite configuración en archivos ini, xml, json o yaml. La configuración es muy similar en lo que se refiere a los nombres de los campos de configuración y el ejemplo lo haré con archivos ini. Para ello crearemos el archivo /etc/uwsgi/apps-available/webservice.ini donde webservice es el nombre de nuestro servicio. Con el siguiente contenido:

1
2
3
4
5
6
7
8
9
10
11
[uwsgi]
socket = 127.0.0.1:9091
chdir = /var/www/myapplication/webservice/
wsgi-file = /var/www/myapplication/webservice/www/webservice.py
pp=/var/www/myapplication/webservice/www
module=webservice
processes = 4
threads = 2
stats = 127.0.0.1:9191
pidfile = /var/run/webservice.pid
callable=application

Se lanzan 4 procesos con 2 hilos cada uno. Y el servidor uWSGI escuchará por el puerto 9091 dentro de la máquina local. Es importante establecer el callable adecuado. Como vemos en nuestra aplicación Python, la línea:

1
application = web.application(urls, globals()).wsgifunc()

Crea el objeto de aplicación llamado application. Ese será nuestro callable. Por último, es recomendable activar las stats. Será otro servidor web escuchando en el puerto 9191 y sólo para conexiones locales. Esto servirá para monitorizar la salud de nuestro servidor y que tengamos algo de control y posibilidad de mejorar el servicio y saber cómo se está comportando.

Luego, haremos un enlace desde /etc/uwsgi/apps-available/webservice.ini a /etc/uwsgi/apps-enabled/webservice.ini, como vemos, es muy parecido a otros sistemas en los que podemos activar y desactivar servicios mediante la creación y borrado de enlaces.

Luego en Nginx, la configuración que colocaremos en /etc/nginx/sites-available/webservice podría ser algo como esto:

1
2
3
4
5
6
7
8
server {
  listen 80;
  server_name apps.example.com;
    location /webservice/ {
    uwsgi_pass 127.0.0.1:9091;
    include uwsgi_params;
  }
}

Tras esto, creamos un enlace desde /etc/nginx/sites-available/webservice a /etc/nginx/sites-enabled/webservice y reiniciamos el servidor Nginx.

Lo bueno del protocolo utilizado por uWSGI es que es mucho más rápido que el HTTP. Por lo que la comunicación entre el servidor web y uWSGI será muy rápido. UWSGI es un protocolo binario, y si viéramos el contenido de dicha comunicación nos costaría mucho entender algo (como humanos), aunque no es un protocolo estándar, y algunas veces no podremos utilizarlo. Hace poco tuve un caso particular en el que no podía instalar el plugin uWSGI para Nginx y tuve que configurar uWSGI para utilizar HTTP para la comunicación con el servidor web.

UWSGI como servidor HTTP

Y este caso utilizaré para el ejemplo a continuación. Dado que uWSGI será un servidor privado no es necesario configurar una conexión HTTPs entre el servidor web y uWSGI. Esta conexión será local o estará dentro de una red interna, por lo que estaríamos introduciendo complejidad extra e innecesaria en la comunicación. De todas formas, la comunicación entre el servidor web y el usuario sí que puede ser HTTPs.

Lo primero, una vez instalado el paquete uWSGI y el plugin para Python es crear el archivo /etc/uwsgi/apps-available/webservice.ini donde webservice es el nombre de nuestro servicio. El contenido del archivo será el siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
[uwsgi]
plugin=python,http
http = webservice:4480
chdir = /var/www/myapplication/webservice/
wsgi-file = /var/www/myapplication/webservice/www/webservice.py
pp=/var/www/myapplication/webservice/www
module=webservice
processes = 4
threads = 2
stats = 127.0.0.1:9191
pidfile = /var/run/webservice.pid
callable=application

Para este caso, se creará un servidor HTTP en el puerto 4480 que utilizará la aplicación situada en /var/www/myapplication/webservice/www/webservice.py. Finalmente en /etc/nginx/sites-available/webservice configuramos la URL /webservice/ como proxy inverso al puerto 4480, ponemos algo así:

1
2
3
4
5
6
7
server {
  listen 80;
  server_name apps.example.com;
    location /webservice/ {
        proxy_pass http://127.0.0.1:4480/;
  }
}

Ya sólo queda crear un enlace desde /etc/nginx/sites-available/webservice a /etc/nginx/sites-enabled/webservice y reiniciar el servidor Nginx.

Foto principal: frank mckenna

También podría interesarte....

There are 18 comments left Ir a comentario

  1. Pingback: Monta microservicios web rápidamente en Python con web.py | PlanetaLibre /

  2. Ernesto Contreras /
    Usando Google Chrome Google Chrome 67.0.3396.79 en Windows Windows NT

    Hola Gaspar, buen escrito. Me queda una duda ya que soy nuevo en este tema. Cómo pruebo el webservices.py desde un navegador ????? Gracias

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 61.0 en Ubuntu Linux Ubuntu Linux

      Cuando ejecutas,
      $ python webservices.py

      te da una URL y un puerto, tienes que poner esos datos en la barra de dirección del navegador. Pero esto es solo para desarrollo, luego cuando estés en producción tendrás que montar un servidor WSGI por ejemplo, como dice el post.

  3. Mario /
    Usando Mozilla Firefox Mozilla Firefox 58.0 en Ubuntu Linux Ubuntu Linux

    Es necesario algun otro programa parte como xammp?

    1. blogs /
      Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

      la uni y https://ilde2.upf.edu/clatmooc/v/hv4

  4. sam /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    such s wonderful post.
    لعبة بادل

  5. alamaan /
    Usando Google Chrome Google Chrome 117.0.0.0 en Windows Windows NT

    يعتبر عزل الأسطح أحد الخطوات الهامة لتحسين جودة المنازل والمباني في الكويت، حيث تعاني الكثير من المباني في منطقة الخليج من مشكلات التسربات والرطوبة التي تؤدي إلى تآكل الجدران وتمزق الأسطح الخارجية. ولحل هذا المشكلة، يتم استخدام عدة تقنيات لعزل الأسطح، مثل استخدام العازل الحراري والمائي الذي يساعد على الحفاظ على الحرارة داخل المنزل وتحسين كفاءة الطاقة، كما يتم استخدام طلاءات خاصة للأسطح التي تساعد على منع التسربات ومنع تأثيرات الرطوبة. وبالإضافة إلى ذلك، يتم استخدام العوازل الصوتية للحد من الضوضاء الخارجية وتحسين جودة الحياة داخل المنازل. بالتالي يتم الاهتمام بعزل الأسطح في الكويت بهدف توفير الحماية والراحة لسكان المنازل وتحسين جودة الحياة في المنطقة.

    شركة عزل اسطح بالكويت

    1. wachus /
      Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

      El fundy https://fundly.com/alfa-4 publicamos los pro

  6. LIVE CASINO /
    Usando Google Chrome Google Chrome 118.0.0.0 en Windows Windows NT

    There’s no doubt i would fully rate it after i read what is the idea about this article. You did a nice job.
    live dealer casinos

  7. spackle /
    Usando Google Chrome Google Chrome 119.0.0.0 en Windows Windows NT

    Exciting times for developers embracing the best of both worlds!

  8. Business Listings /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Python provides a number of frameworks for web development. This makes it an excellent alternative for those who are just getting started in the world of programming. Python has advantages and disadvantages.

  9. American Sports League /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Excellent to the point article and news.. Well appreciated, My sites:American Sports League – Famous Tournaments in America

  10. AL fahd /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    أهمية مكافحة البق:
    البق ليس فقط مزعجًا، بل يشكل خطرًا على الصحة والنظافة العامة. تأتي أهمية مكافحته من خلال الحفاظ على المأكولات آمنة وضمان سلامة البيئة المحيطة.

    أساليب فعّالة لمكافحة البق:
    1. استخدام المبيدات البيئية:
    تعتبر المبيدات البيئية الخيار الأمثل للتخلص من البق دون التأثير السلبي على البيئة. اختر المبيدات التي تحقق فعالية عالية وفاعلية طويلة الأمد.

    2. تحسين النظافة الشخصية:
    ضمان نظافة المنزل والملابس يلعب دورًا حاسمًا في الوقاية من انتشار البق. اغسل الملابس بانتظام وحافظ على نظافة الأماكن التي يمكن أن يختبئ فيها البق.

    3. استدعاء فريق محترف:
    في بعض الحالات، قد يكون الأمر أفضل بكثير باستدعاء فريق محترف لمكافحة البق. يمتلكون الخبرة والمعرفة لتقديم حلاً فعّالاً وسريعاً.

    ختام:
    باختصار، مكافحة البق في الكويت تتطلب خطوات فعّالة وسريعة. اعتماد الحلول الصحيحة والتدابير الوقائية يمكن أن يحقق فارقًا كبيرًا في الحفاظ على بيئة نظيفة وصحية. لا تتردد في اتخاذ الخطوات اللازمة للتغلب على هذا التحدي وضمان سلامة منزلك وعائلتك.

  11. slope /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    Your writings stick out to me since the content is interesting and simple to understand. Even though I’ve read a lot of websites, I still like yours more. Your essay was interesting to read. I can understand the essay better now that I’ve read it carefully. In the future, I’d like to read more of your writing.

  12. Fool Me Once Maya Stern Black Blazer /
    Usando Google Chrome Google Chrome 120.0.0.0 en Windows Windows NT

    This is excellent article, thank you for the share! This is what I am looking for, hope in future you will continue sharing such an superb work.

  13. Blackjack Online Philippines /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    Excellent to the point article and news.. Well appreciated, My sites:

  14. www.drywallgreensboro.com /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    Exciting times for tech enthusiasts! 🚀

  15. fnaf security breach /
    Usando Google Chrome Google Chrome 121.0.0.0 en Windows Windows NT

    The piece is pretty good. I found your blog by accident and wanted to tell you how much I enjoy reading your posts. I’ll subscribe to your feed either way, and I hope you’ll post again soon. Many thanks for the helpful info. You can play: fnaf security breach to relax, or pass the time!

Leave a Reply to Ernesto Contreras Cancle Reply