DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Kubernetes es una gran herramienta para ejecutar contenedores Docker en un entorno de producción en clúster. Sin embargo, existen problemas que Kubernetes no puede resolver. Para implementaciones de producción frecuentes, necesitamos una implementación azul/verde totalmente automatizada para evitar el tiempo de inactividad en el proceso, que también debe manejar solicitudes HTTP externas y realizar descargas de SSL. Esto requiere integración con un equilibrador de carga como ha-proxy. Otro desafío es el escalamiento semiautomático del propio clúster de Kubernetes cuando se ejecuta en un entorno de nube, por ejemplo, reducir parcialmente el clúster por la noche.

Si bien Kubernetes no tiene estas características listas para usar, proporciona una API que puede usar para resolver problemas similares. Se desarrollaron herramientas para la implementación y el escalado azul/verde automatizados de un clúster de Kubernetes como parte del proyecto Cloud RTI, que se creó en base a código abierto.

Este artículo, una transcripción de video, le muestra cómo configurar Kubernetes junto con otros componentes de código abierto para crear un entorno listo para producción que acepte código de una confirmación de git sin tiempo de inactividad en producción.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 1

Entonces, una vez que tenga acceso a sus aplicaciones desde el mundo exterior, puede comenzar a configurar completamente la automatización, es decir, llevarla a la etapa en la que pueda realizar una confirmación de git y asegurarse de que esta confirmación de git termine en producción. Naturalmente, al implementar estos pasos, al implementar la implementación, no queremos encontrarnos con tiempo de inactividad. Entonces, cualquier automatización en Kubernetes comienza con la API.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Kubernetes no es una herramienta que pueda utilizarse de forma productiva desde el primer momento. Por supuesto, puedes hacer eso, usar kubectl y demás, pero aun así la API es lo más interesante y útil de esta plataforma. Al utilizar la API como un conjunto de funciones, puede acceder a casi cualquier cosa que desee hacer en Kubernetes. El propio kubectl también utiliza la API REST.

Esto es REST, por lo que puede usar cualquier lenguaje o herramienta para trabajar con esta API, pero las bibliotecas personalizadas le harán la vida mucho más fácil. Mi equipo escribió 2 bibliotecas de este tipo: una para Java/OSGi y otra para Go. El segundo no se usa mucho, pero en cualquier caso tienes estas cosas útiles a tu disposición. Son un proyecto de código abierto con licencia parcial. Existen muchas bibliotecas de este tipo para diferentes idiomas, por lo que puede elegir la que más le convenga.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Por lo tanto, antes de comenzar a automatizar su implementación, debe asegurarse de que el proceso no estará sujeto a ningún tiempo de inactividad. Por ejemplo, nuestro equipo lleva a cabo implementaciones de producción durante el mediodía, cuando las personas utilizan las aplicaciones al máximo, por lo que es importante evitar retrasos en este proceso. Para evitar el tiempo de inactividad, se utilizan dos métodos: implementación azul/verde o actualización continua. En este último caso, si tienes 2 réplicas de la aplicación ejecutándose, se actualizan secuencialmente una tras otra. Este método funciona muy bien, pero no es adecuado si tiene diferentes versiones de la aplicación ejecutándose simultáneamente durante el proceso de implementación. En este caso, puede actualizar la interfaz de usuario mientras el backend ejecuta la versión anterior y la aplicación dejará de funcionar. Por tanto, desde el punto de vista de la programación, trabajar en tales condiciones es bastante complicado.

Esta es una de las razones por las que preferimos utilizar la implementación azul/verde para automatizar la implementación de nuestras aplicaciones. Con este método, debes asegurarte de que solo una versión de la aplicación esté activa a la vez.

El mecanismo de implementación azul/verde se ve así. Recibimos tráfico para nuestras aplicaciones a través de ha-proxy, que lo reenvía a réplicas en ejecución de la aplicación de la misma versión.

Cuando se realiza una nueva implementación, utilizamos Deployer, al que se le asignan los nuevos componentes y se implementa la nueva versión. Implementar una nueva versión de una aplicación significa que se "genera" un nuevo conjunto de réplicas, después de lo cual estas réplicas de la nueva versión se lanzan en un pod nuevo e independiente. Sin embargo, ha-proxy no sabe nada sobre ellos y aún no les envía ninguna carga de trabajo.

Por lo tanto, en primer lugar, es necesario realizar una verificación de rendimiento de las nuevas versiones de verificación de estado para garantizar que las réplicas estén listas para atender la carga.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Todos los componentes de implementación deben admitir algún tipo de verificación de estado. Esto puede ser una verificación de llamada HTTP muy simple, cuando recibe un código con estado 200, o una verificación más profunda, en la que verifica la conexión de las réplicas con la base de datos y otros servicios, la estabilidad de las conexiones del entorno dinámico. y si todo arranca y funciona correctamente. Este proceso puede ser bastante complejo.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Después de que el sistema verifique que todas las réplicas actualizadas estén funcionando, Deployer actualizará la configuración y pasará la configuración correcta, lo que reconfigurará ha-proxy.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Solo después de esto, el tráfico se dirigirá al pod con réplicas de la nueva versión y el pod anterior desaparecerá.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Este mecanismo no es una característica de Kubernetes. El concepto de implementación azul/verde existe desde hace bastante tiempo y siempre ha utilizado un equilibrador de carga. Primero, dirige todo el tráfico a la versión anterior de la aplicación y, después de la actualización, lo transfiere completamente a la nueva versión. Este principio se utiliza no sólo en Kubernetes.

Ahora les presentaré un nuevo componente de implementación: Deployer, que realiza comprobaciones de estado, reconfigura los servidores proxy, etc. Este es un concepto que no se aplica al mundo exterior y existe dentro de Kubernetes. Te mostraré cómo puedes crear tu propio concepto de Deployer usando herramientas de código abierto.

Entonces, lo primero que hace Deployer es crear un controlador de replicación RC usando la API de Kubernetes. Esta API crea pods y servicios para su posterior implementación, es decir, crea un clúster completamente nuevo para nuestras aplicaciones. Tan pronto como RC esté convencido de que las réplicas se han iniciado, realizará una verificación de estado de su funcionalidad. Para hacer esto, Deployer usa el comando GET /health. Ejecuta los componentes de escaneo apropiados y verifica todos los elementos que respaldan el funcionamiento del clúster.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Una vez que todos los pods han informado sobre su estado, Deployer crea un nuevo elemento de configuración: almacenamiento distribuido etcd, que Kubernetes utiliza internamente, incluido el almacenamiento de la configuración del equilibrador de carga. Escribimos datos en etcd, y una pequeña herramienta llamada confd monitorea etcd en busca de nuevos datos.

Si detecta algún cambio en la configuración inicial, genera un nuevo archivo de configuración y lo transfiere a ha-proxy. En este caso, ha-proxy se reinicia sin perder ninguna conexión y dirige la carga a nuevos servicios que permitan que funcione la nueva versión de nuestras aplicaciones.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Como puede ver, a pesar de la abundancia de componentes, aquí no hay nada complicado. Sólo necesitas prestar más atención a la API y etc. Quiero hablarles sobre el implementador de código abierto que nosotros mismos utilizamos: Amdatu Kubernetes Deployer.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Es una herramienta para orquestar implementaciones de Kubernetes y tiene las siguientes características:

  • Despliegue Azul/Verde;
  • configurar un equilibrador de carga externo;
  • gestión de descriptores de implementación;
  • gestionar el despliegue real;
  • comprobar la funcionalidad de los controles de estado durante la implementación;
  • Implementación de variables de entorno en pods.

Este implementador está construido sobre la API de Kubernetes y proporciona una API REST para administrar identificadores e implementaciones, así como una API Websocket para transmitir registros durante el proceso de implementación.

Coloca los datos de configuración del balanceador de carga en etcd, por lo que no es necesario usar ha-proxy con soporte listo para usar, pero puede usar fácilmente su propio archivo de configuración del balanceador de carga. Amdatu Deployer está escrito en Go, como el propio Kubernetes, y tiene licencia de Apache.

Antes de comenzar a usar esta versión del implementador, usé el siguiente descriptor de implementación, que especifica los parámetros que necesito.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Uno de los parámetros importantes de este código es habilitar el indicador "useHealthCheck". Necesitamos especificar que se debe realizar una verificación de integridad durante el proceso de implementación. Esta configuración se puede deshabilitar cuando la implementación utiliza contenedores de terceros que no necesitan ser verificados. Este descriptor también indica la cantidad de réplicas y la URL de interfaz que necesita ha-proxy. Al final está el indicador de especificación del pod "podspec", que llama a Kubernetes para obtener información sobre la configuración del puerto, la imagen, etc. Este es un descriptor JSON bastante simple.

Otra herramienta que forma parte del proyecto de código abierto Amdatu es Deploymentctl. Tiene una interfaz de usuario para configurar implementaciones, almacena el historial de implementaciones y contiene webhooks para devoluciones de llamadas de usuarios y desarrolladores externos. No puede utilizar la interfaz de usuario ya que Amdatu Deployer en sí es una API REST, pero esta interfaz puede facilitarle mucho la implementación sin involucrar ninguna API. Deploymentctl está escrito en OSGi/Vertx usando Angular 2.

Ahora demostraré lo anterior en pantalla usando una grabación pregrabada para que no tengas que esperar. Implementaremos una aplicación Go simple. No te preocupes si no has probado Go antes, es una aplicación muy sencilla, por lo que deberías poder entenderla.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Aquí estamos creando un servidor HTTP que solo responde a /health, por lo que esta aplicación solo prueba la verificación de estado y nada más. Si la verificación pasa, se utiliza la estructura JSON que se muestra a continuación. Contiene la versión de la aplicación que implementará el implementador, el mensaje que ve en la parte superior del archivo y el tipo de datos booleanos, ya sea que nuestra aplicación esté funcionando o no.

Hice un poco de trampa con la última línea, porque coloqué un valor booleano fijo en la parte superior del archivo, que en el futuro me ayudará a implementar incluso una aplicación "en mal estado". Nos ocuparemos de esto más tarde.

Entonces empecemos. Primero, verificamos la presencia de pods en ejecución usando el comando ~ kubectl get pods y, en función de la ausencia de una respuesta de la URL del frontend, nos aseguramos de que no se estén realizando implementaciones actualmente.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

A continuación, en la pantalla verá la interfaz Deploymentctl que mencioné, en la que se configuran los parámetros de implementación: espacio de nombres, nombre de la aplicación, versión de implementación, número de réplicas, URL de front-end, nombre del contenedor, imagen, límites de recursos, número de puerto para verificación de estado, etc. Los límites de recursos son muy importantes, ya que le permiten utilizar la máxima cantidad posible de hardware. Aquí también puede ver el registro de implementación.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Si ahora repite el comando ~ kubectl get pods, podrá ver que el sistema se “congela” durante 20 segundos, durante los cuales se reconfigura ha-proxy. Después de esto, se inicia el pod y nuestra réplica se puede ver en el registro de implementación.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Corté la espera de 20 segundos del video y ahora puedes ver en la pantalla que se ha implementado la primera versión de la aplicación. Todo esto se hizo utilizando únicamente la interfaz de usuario.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Ahora probemos la segunda versión. Para hacer esto, cambio el mensaje de la aplicación de "¡Hola, Kubernetes!" en “¡Hola, Deployer!”, el sistema crea esta imagen y la coloca en el registro de Docker, luego de lo cual simplemente hacemos clic en el botón “Implementar” nuevamente en la ventana Deploymentctl. En este caso, el registro de implementación se inicia automáticamente de la misma manera que sucedió al implementar la primera versión de la aplicación.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

El comando ~ kubectl get pods muestra que actualmente hay 2 versiones de la aplicación ejecutándose, pero la interfaz muestra que todavía estamos ejecutando la versión 1.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

El equilibrador de carga espera a que se complete la verificación de estado antes de redirigir el tráfico a la nueva versión. Después de 20 segundos, cambiamos a curl y vemos que ahora tenemos implementada la versión 2 de la aplicación y la primera ha sido eliminada.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Este fue el despliegue de una aplicación "saludable". Veamos qué sucede si para una nueva versión de la aplicación cambio el parámetro Saludable de verdadero a falso, es decir, intento implementar una aplicación en mal estado que no pasó la verificación de estado. Esto puede suceder si se cometieron algunos errores de configuración en la aplicación en la etapa de desarrollo y se envió a producción de este formulario.

Como puede ver, la implementación sigue todos los pasos anteriores y ~kubectl get pods muestra que ambos pods se están ejecutando. Pero a diferencia de la implementación anterior, el registro muestra el estado del tiempo de espera. Es decir, debido a que la verificación de estado falló, no se puede implementar la nueva versión de la aplicación. Como resultado, verá que el sistema ha vuelto a utilizar la versión anterior de la aplicación y la nueva versión simplemente se ha desinstalado.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Lo bueno de esto es que incluso si tiene una gran cantidad de solicitudes simultáneas ingresando a la aplicación, ni siquiera notarán el tiempo de inactividad mientras implementan el procedimiento de implementación. Si prueba esta aplicación utilizando el marco Gatling, que le envía tantas solicitudes como sea posible, ninguna de estas solicitudes se descartará. Esto significa que nuestros usuarios ni siquiera notarán las actualizaciones de versiones en tiempo real. Si falla, el trabajo continuará en la versión anterior; si tiene éxito, los usuarios cambiarán a la nueva versión.

Solo hay una cosa que puede fallar: si la verificación de estado tiene éxito, pero la aplicación falla tan pronto como se le aplica la carga de trabajo, es decir, el colapso ocurrirá solo después de que se complete la implementación. En este caso, deberá volver manualmente a la versión anterior. Entonces, analizamos cómo usar Kubernetes con las herramientas de código abierto diseñadas para ello. El proceso de implementación será mucho más fácil si integra estas herramientas en sus canales de compilación/implementación. Al mismo tiempo, para iniciar la implementación, puede utilizar la interfaz de usuario o automatizar completamente este proceso utilizando, por ejemplo, comprometerse con el maestro.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Nuestro Build Server creará una imagen de Docker y la insertará en Docker Hub o en cualquier registro que utilice. Docker Hub admite webhook, por lo que podemos activar la implementación remota a través de Deployer de la forma que se muestra arriba. De esta manera, puede automatizar completamente la implementación de su aplicación en producción potencial.

Pasemos al siguiente tema: escalar el clúster de Kubernetes. Tenga en cuenta que el comando kubectl es un comando de escala. Con más ayuda, podemos aumentar fácilmente la cantidad de réplicas en nuestro clúster existente. Sin embargo, en la práctica, normalmente queremos aumentar la cantidad de nodos en lugar de pods.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Al mismo tiempo, durante el horario laboral es posible que deba aumentar y, por la noche, para reducir el costo de los servicios de Amazon, es posible que deba reducir la cantidad de instancias de aplicaciones en ejecución. Esto no significa que escalar solo el número de pods será suficiente, porque incluso si uno de los nodos está inactivo, igual tendrás que pagarle a Amazon por él. Es decir, además de escalar los pods, deberá escalar la cantidad de máquinas utilizadas.

Esto puede ser un desafío porque ya sea que usemos Amazon u otro servicio en la nube, Kubernetes no sabe nada sobre la cantidad de máquinas que se utilizan. Carece de una herramienta que le permita escalar el sistema a nivel de nodo.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Entonces tendremos que cuidar tanto los nodos como los pods. Podemos escalar fácilmente el lanzamiento de nuevos nodos utilizando la API de AWS y las máquinas del grupo de escalamiento para configurar la cantidad de nodos trabajadores de Kubernetes. También puede utilizar cloud-init o un script similar para registrar nodos en el clúster de Kubernetes.

La nueva máquina inicia en el grupo Scaling, se inicia como nodo, se registra en el registro del maestro y comienza a funcionar. Después de esto, puede aumentar la cantidad de réplicas para usar en los nodos resultantes. La reducción requiere más esfuerzo, ya que es necesario asegurarse de que dicho paso no conduzca a la destrucción de las aplicaciones que ya se están ejecutando después de apagar máquinas "innecesarias". Para evitar tal escenario, debe configurar los nodos en el estado "no programable". Esto significa que el programador predeterminado ignorará estos nodos al programar pods de DaemonSet. El programador no eliminará nada de estos servidores, pero tampoco lanzará nuevos contenedores allí. El siguiente paso es eliminar el nodo de drenaje, es decir, transferir los pods en ejecución a otra máquina u otros nodos que tengan suficiente capacidad para esto. Una vez que se haya asegurado de que ya no hay contenedores en estos nodos, puede eliminarlos de Kubernetes. Después de esto, simplemente dejarán de existir para Kubernetes. A continuación, debe utilizar la API de AWS para desactivar máquinas o nodos innecesarios.
Puede utilizar Amdatu Scalerd, otra herramienta de escalado de código abierto similar a la API de AWS. Proporciona una CLI para agregar o eliminar nodos en un clúster. Su característica interesante es la capacidad de configurar el programador utilizando el siguiente archivo json.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

El código mostrado reduce la capacidad del clúster a la mitad durante el período nocturno. Configura tanto la cantidad de réplicas disponibles como la capacidad deseada del clúster de Amazon. El uso de este programador reducirá automáticamente la cantidad de nodos por la noche y los aumentará por la mañana, ahorrando el costo de usar nodos de un servicio en la nube como Amazon. Esta característica no está integrada en Kubernetes, pero usar Scalerd le permitirá escalar esta plataforma como desee.

Me gustaría señalar que mucha gente me dice: "Eso está muy bien, pero ¿qué pasa con mi base de datos, que normalmente es estática?" ¿Cómo se puede ejecutar algo como esto en un entorno dinámico como Kubernetes? En mi opinión, no deberías hacer esto, no deberías intentar ejecutar un almacén de datos en Kubernetes. Esto es técnicamente posible y hay tutoriales en Internet sobre este tema, pero te complicará la vida seriamente.

Sí, existe un concepto de almacenes persistentes en Kubernetes y puede intentar ejecutar almacenes de datos como Mongo o MySQL, pero esta es una tarea que requiere bastante mano de obra. Esto se debe al hecho de que los almacenes de datos no soportan completamente la interacción con un entorno dinámico. La mayoría de las bases de datos requieren una configuración importante, incluida la configuración manual del clúster, no les gusta el escalado automático y otras cosas similares.
Por lo tanto, no deberías complicarte la vida intentando ejecutar un almacén de datos en Kubernetes. Organice su trabajo de forma tradicional utilizando servicios familiares y simplemente proporcione a Kubernetes la posibilidad de utilizarlos.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Para concluir el tema, me gustaría presentarles la plataforma Cloud RTI basada en Kubernetes, en la que está trabajando mi equipo. Proporciona registro centralizado, monitoreo de aplicaciones y clústeres, y muchas otras características útiles que serán útiles. Utiliza varias herramientas de código abierto, como Grafana, para mostrar el seguimiento.

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

DEVOXX Reino Unido. Kubernetes en producción: implementación azul/verde, escalado automático y automatización de implementación. Parte 2

Hubo una pregunta sobre por qué utilizar el balanceador de carga ha-proxy con Kubernetes. Buena pregunta porque actualmente existen 2 niveles de equilibrio de carga. Los servicios de Kubernetes todavía residen en direcciones IP virtuales. No puede usarlos para puertos en máquinas host externas porque si Amazon sobrecarga su host en la nube, la dirección cambiará. Es por eso que colocamos ha-proxy delante de los servicios, para crear una estructura más estática para que el tráfico se comunique sin problemas con Kubernetes.

Otra buena pregunta es ¿cómo puede encargarse de los cambios en el esquema de la base de datos al realizar una implementación azul/verde? El hecho es que, independientemente del uso de Kubernetes, cambiar el esquema de la base de datos es una tarea difícil. Debe asegurarse de que el esquema antiguo y el nuevo sean compatibles, después de lo cual podrá actualizar la base de datos y luego actualizar las aplicaciones. Puede intercambiar la base de datos en caliente y luego actualizar las aplicaciones. Conozco personas que han iniciado un clúster de base de datos completamente nuevo con un nuevo esquema; esta es una opción si tienes una base de datos sin esquema como Mongo, pero de todos modos no es una tarea fácil. Si no tienes más preguntas, ¡gracias por tu atención!

Algunos anuncios 🙂

Gracias por estar con nosotros. ¿Te gustan nuestros artículos? ¿Quieres ver más contenido interesante? Apóyanos haciendo un pedido o recomendándonos a amigos, VPS en la nube para desarrolladores desde $4.99, un análogo único de servidores de nivel de entrada, que fue inventado por nosotros para usted: Toda la verdad sobre VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps desde $19 o como compartir servidor? (disponible con RAID1 y RAID10, hasta 24 núcleos y hasta 40GB DDR4).

Dell R730xd 2 veces más barato en el centro de datos Equinix Tier IV en Amsterdam? Solo aqui 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TV desde $199 ¡en los Paises Bajos! Dell R420 - 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB - ¡desde $99! Leer acerca de Cómo construir infraestructura corp. clase con el uso de servidores Dell R730xd E5-2650 v4 por valor de 9000 euros por un centavo?

Fuente: habr.com

Añadir un comentario