Volver al blog

Desplegar una aplicación PHP en un clúster de Kubernetes con Ubuntu 18.04

Desplegar una aplicación PHP en un clúster de Kubernetes con Ubuntu 18.04

Kubernetes (también conocido como k8s) es un sistema de orquestación de código abierto. Permite a los usuarios implementar, escalar y administrar aplicaciones en contenedores con un tiempo de inactividad mínimo. En este tutorial, aprenderá a implementar una aplicación PHP en un clúster de Kubernetes.

Nginx se comporta como un proxy para PHP-FPM mientras se ejecuta una aplicación PHP. Administrar estos dos servicios en un solo contenedor es un proceso difícil. Kubernetes nos ayuda a administrarlos en dos contenedores diferentes y reduce las complicaciones. También permite a los usuarios reutilizar los contenedores y no preocuparse por compilar su imagen de contenedor para cada nueva versión de PHP/Nginx.

Ejecutará su aplicación y el servicio de proxy en dos contenedores separados. El tutorial también proporcionará información sobre cómo utilizar el almacenamiento local para crear un Volumen Persistente (PV) y una Solicitud de Volumen Persistente (PVC). Luego, utilizará esta PVC para mantener sus archivos de configuración y código fuera de las imágenes de contenedor. Después de completar este tutorial, podrá reutilizar su imagen de Nginx para otras aplicaciones que requieran un servidor proxy. Puede lograr esto pasando una configuración, en lugar de reconstruir la imagen para ello.

Requisitos previos

  1. Una comprensión básica de Kubernetes (k8s) y sus objetos. Consulte esta guía para obtener una visión detallada del ecosistema de Kubernetes.
  2. Un clúster de Kubernetes que esté activo y en funcionamiento en Ubuntu 18.04. Siga este tutorial para crear su clúster de Kubernetes usando kubeadm.
  3. Además, debe alojar el código de su aplicación en una URL pública, por ejemplo, GitHub.

Paso 1: Crear los servicios PHP-FPM y Nginx

Este paso le ayudará a crear los servicios PHP-FPM y Nginx. Cualquier servicio proporciona acceso a un conjunto de pods dentro de un clúster. Todos los servicios presentes en un clúster pueden comunicarse entre sí con sus nombres, sin direcciones IP. El servicio PHP-FPM y el servicio Nginx proporcionarán acceso a los pods de PHP-FPM y Nginx, respectivamente.

Deberá indicarle al servicio PHP-FPM cómo encontrar los pods de Nginx, ya que actuará como un proxy para los pods de PHP-FPM. Para esto, aprovechará el descubrimiento automático de servicios de Kubernetes’ y utilizará nombres legibles por humanos para enrutar la solicitud al servicio respectivo.

Para crear cualquier servicio, deberá crear un archivo YAML que contenga la definición del objeto. Este archivo YAML tiene al menos las siguientes etiquetas:

  1. apiVersion: La versión de la API de Kubernetes a la que pertenece la definición.
  2. kind: El tipo de objeto de Kubernetes que crea este archivo YAML. Por ejemplo: un service, un job, o un pod.
  3. metadata: El nombre del objeto y las diferentes labels que el usuario podría querer aplicar a este objeto se definen bajo esta etiqueta.
  4. spec: Esta etiqueta contiene la especificación del objeto, como las variables de entorno (ENV), la imagen de contenedor que se utilizará y los puertos en los que el servicio del contenedor estará accesible.
Creación del servicio PHP-FPM

Para empezar, debe crear un directorio para guardar la definición de su objeto de Kubernetes. Inicie sesión en su nodo maestro y cree un directorio llamado “definitions:”

Cambie al directorio definitions:

A continuación, cree su archivo de servicio PHP-FPM como el archivo php_service.yaml:

Después de eso, establezca apiVersion y kind en el archivo php_fpm_service.yaml:

Nombre su servicio como php o php-fpm ya que proporcionará acceso a su aplicación PHP-FPM:

Etiquete su servicio php como tier: backend ya que la aplicación PHP se ejecutará detrás de este servicio:

Un servicio utiliza las etiquetas de selector para determinar a qué pods acceder. Cualquier pod que coincida con estas etiquetas, independientemente de cuándo se haya creado, recibirá servicio. Aprenderá cómo agregar etiquetas a sus pods más adelante en este tutorial.

Incluye la etiqueta tier: backend que asigna tu pod a la capa backend, junto con la etiqueta app: php-fpm para indicar que el pod ejecuta una aplicación PHP-FPM. Debes añadir estas etiquetas después de la sección metadata:

A continuación, debes declarar el puerto para acceder a este servicio php-fpm bajo spec. Puedes añadir cualquier puerto de tu elección, pero usaremos el puerto 9000 en este tutorial:

Una vez completados los pasos anteriores, tu archivo php_fpm_service.yaml se verá así:

Presiona Ctrl + O para guardar el archivo, luego presiona Ctrl + X para salir de nano.

Aplicar el comando kubectl para crear el servicio PHP

Una vez creada la definición del objeto para tu servicio, ejecuta el comando kubectl apply con el argumento -f especificando tu archivo php_fpm_service.yaml:

La salida del comando anterior debería ser:

Ejecuta el siguiente comando para verificar que tu servicio php-fpm se está ejecutando:

Podrás ver el servicio php-fpm activo y en ejecución:

Nota: Kubernetes admite varios tipos de servicios. Tu servicio php-fpm utiliza el tipo de servicio predeterminado ClusterIP. Este tipo de servicio asigna una IP interna y hace que el servicio sea accesible únicamente desde dentro del clúster de Kubernetes.
Creación del servicio Nginx

Dado que tu servicio PHP-FPM ya está listo, es hora de que también crees tu servicio Nginx. Crea y abre un nuevo archivo YAML para este servicio, llamado nginx_service.yaml en el editor:

Nombra este servicio como nginx ya que se dirigirá a los pods de Nginx. Este servicio también pertenece al backend, por lo que debes añadirle una etiqueta tier: backend:

Al igual que hicimos en el servicio php-fpm, añade las etiquetas de selector app: nginx y tier: backend para dirigirte a los pods. Añade el puerto HTTP predeterminado 80 para acceder a este servicio:

Se puede acceder públicamente al servicio Nginx en Internet desde la dirección IP pública. Puedes añadir la IP de tu nodo de trabajo como your_public_ip. Añade las siguientes líneas bajo spec.externalIPs:

Tu archivo nginx_service.yaml debería verse como el siguiente una vez que completes todos los pasos anteriores:

Guarda y cierra el archivo después de añadir todos los parámetros requeridos anteriormente.

Aplicar el comando kubectl para crear el servicio Nginx
Debería ver la siguiente salida para el comando anterior:
Ahora, ejecute el siguiente comando para ver todos sus servicios en ejecución:
Al ejecutar el comando anterior, debería poder ver tanto su servicio PHP-FPM como el de Nginx activos y en funcionamiento:
Tenga en cuenta que si desea eliminar cualquiera de sus servicios en ejecución, puede ejecutar el siguiente comando:

Paso 2: Crear almacenamiento local y volumen persistente

Kubernetes proporciona varios complementos de almacenamiento que le ayudan a crear espacio de almacenamiento para su entorno. Este paso le guiará sobre cómo crear una StorageClass local y cómo se puede utilizar esta clase de almacenamiento para crear un volumen persistente.

Crear un almacenamiento local

Cree un archivo, por ejemplo, storageClass.yaml, en su editor:

Añada kind como "storageClass" y apiVersion como "storage.k8s.io/v1" de la siguiente manera:

Nombre esta StorageClass como "my-local-storage" y añada provisioner y volumeBindingMode de la siguiente manera:

Guarde y salga del archivo y su archivo final storageClass.yaml debería verse así:

Ahora, cree la StorageClass ejecutando el comando kubectl create, como se muestra a continuación:

Después de ejecutar el comando anterior, debería obtener la siguiente salida:

Crear un volumen persistente local

Después de crear el almacenamiento local, puede crear su volumen persistente local. Un volumen persistente, también conocido como PV, es el almacenamiento de bloques de tamaño especificado que es independiente del ciclo de vida de un pod. Un volumen persistente local no es más que un disco local o un directorio que está disponible en un nodo del clúster de Kubernetes. Este volumen persistente local permite a sus usuarios acceder al almacenamiento local mediante una reclamación de volumen persistente local de una manera muy sencilla pero portátil. Puede crear este volumen persistente local utilizando esta clase de almacenamiento que acabamos de crear. Abra un archivo, por ejemplo, persistentVolume.yaml, en su editor:

Asigne un nombre a este volumen persistente, por ejemplo, "my-local-pv":

Puede añadir capacidad de almacenamiento según su uso al crear un volumen persistente local. En este tutorial, utilizaremos 5 Gi para el almacenamiento:

Añada accessModes, persistentVolumeReclaimPolicy y proporcione el mismo storageClassName que se utilizó en storageClass.yaml:

Nota: persistentVolumeReclaimPolicy le indica qué sucede con el volumen persistente (Persistent Volume) una vez que se libera su reclamación (Persistent Volume Claim). Hay tres opciones válidas para este parámetro: Retain, Delete y Recycle. En nuestro código, utilizaremos la opción Retain. Para obtener más detalles, puede consultar el campo persistentVolumeReclaimPolicy aquí: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#persistentvolumeclaim-v1-core

Agregue el local.path para su volumen persistente (Persistent Volume) como se muestra a continuación:

Nota: Asegúrese de que esta ruta local (/mnt/disk/vol) exista en el nodo de su clúster de Kubernetes.

Después de agregar todos los campos requeridos, su archivo persistentVolume.yaml debería verse así:

Nota: Debe utilizar el nombre de nodo correcto de su máquina. En este caso, es: “worker.”
Preparación del volumen local

Ahora, necesitamos preparar un volumen local en el nodo “worker” tal como lo hemos agregado en el archivo persistentVolume.yaml . Ejecute los siguientes comandos en el nodo que ha configurado en persistentVolume. En este caso, es el nodo “worker”:

Nota: Asegúrese de tener los permisos suficientes para crear el directorio y cambiar los permisos como se muestra arriba. Si no es así, ejecute los comandos con el usuario correcto.

Ejecute el siguiente comando en el nodo maestro donde su archivo persistentVolume.yaml está presente:

Debería obtener la siguiente salida:

Dado que ha creado con éxito su almacenamiento local y su volumen persistente (Persistent Volume), ahora puede proceder a crear una reclamación de volumen persistente (Persistent Volume Claim) para contener el código de su aplicación y los archivos de configuración.

Paso 3: Crear el volumen persistente

El código de su aplicación debe mantenerse seguro mientras administra o actualiza sus pods. Para ello, utilizará el volumen persistente (Persistent Volume) creado en el paso anterior, al cual se accede mediante una reclamación de volumen persistente (Persistent Volume Claim) o PVC. Esta PVC monta el PV en la ruta requerida.

Abra un archivo, por ejemplo, code_volume.yaml, en su editor:

Nombre su PVC como code agregando los siguientes parámetros y valores a su archivo:

La sección spec de una PVC tiene los siguientes elementos:

  1. accessModes: Existen varios valores posibles para este campo, los cuales se detallan a continuación:
    • ReadWriteOnce – Monta el volumen para un solo nodo con permisos tanto de lectura como de escritura.
    • ReadOnlyMany – Monta el volumen para muchos nodos con permiso de solo lectura.
    • ReadWriteMany – Monta el volumen para muchos nodos con permisos tanto de lectura como de escritura.
  2. resources: Define el espacio de almacenamiento requerido.

Dado que el almacenamiento local está montado en un solo nodo, deberá establecer el accessMode en ReadWriteOnce. En este tutorial agregará solo una pequeña parte de código de aplicación, por lo que 1 GB de almacenamiento será suficiente aquí. Sin embargo, si desea almacenar una mayor cantidad de datos o código, puede modificar el parámetro de almacenamiento de acuerdo con sus requisitos. Tenga en cuenta que una vez creado el volumen, podrá aumentar el tamaño del almacenamiento. Sin embargo, no se admite la reducción del mismo:

Ahora, declare la clase de almacenamiento que el clúster de Kubernetes utilizará para asignar a los volúmenes. Utilice la clase de almacenamiento my-local-storage, creada en el paso anterior, aquí para su storageClassName:

Después de completar los pasos anteriores, su archivo code_volume.yaml debería verse así:

Ahora guarde y salga del archivo.

Creación de PVC

Cree el PVC de código ejecutando el comando kubectl apply:

Debería obtener la siguiente salida que indica que el objeto se creó correctamente y está listo para poder montar su PVC de 1 GB como volumen:

Puede ejecutar el siguiente comando para verificar el volumen persistente (PV) disponible:

La salida del comando anterior debería ser la siguiente:

Todos los campos anteriores, excepto Reclaim Policy y Status, son una descripción general de su archivo de configuración. Reclaim Policy define qué sucede con el PV una vez que se elimina el PVC que accede a él. El valor Delete elimina el PV tanto del clúster de Kubernetes como de la infraestructura de almacenamiento. Puede consultar la documentación de PV de Kubernetes para tener una comprensión clara de Reclaim Policy y Status.

Ahora puede crear sus pods utilizando un Deployment, ya que ha creado correctamente su volumen persistente utilizando el almacenamiento local.

Paso 4: Crear un Deployment para su aplicación PHP-FPM

Este paso le ayudará a crear su pod PHP-FPM utilizando Deployment. Deployment utiliza ReplicaSets para proporcionar una forma estable de crear, actualizar y administrar sus pods. Un Deployment revierte automáticamente sus pods a una imagen anterior.

La clave spec.selector en el Deployment enumera todas las etiquetas de los pods que administra. También utiliza la clave template para crear los pods que se requieren.

En este paso, también introduciremos la aplicación de Init Containers. Los Init Containers ejecutan algunos comandos antes de los contenedores regulares que se especifican bajo la plantilla del pod’s. Aquí, el Init Container utilizará GitHub Gist (https://gist.github.com/) para obtener un index.php de muestra. El contenido del archivo de muestra es:

Creación del Deployment de PHP

Abra un nuevo archivo llamado php_deployment.yaml en su editor para crear su Deployment:

Ahora, nombre al objeto Deployment PHP, ya que este Deployment administrará sus pods PHP-FPM. Agregue la etiqueta tier: backend porque el pod pertenecerá a la capa backend:

Usando el parámetro replica, especifique el número de copias de este pod que se deben crear. El número de réplicas puede variar según sus requisitos y los recursos disponibles. En este tutorial, creará solo una réplica de su pod:

Agregue app: php y tier:backend etiquetas bajo la clave selector que denota que este Deployment administrará los pods que coincidan con estas dos etiquetas:

Ahora, la definición de objeto de su pod necesita una plantilla bajo la especificación de su Deployment. Esta plantilla define la especificación que se necesita para crear su pod. Para empezar, agregue las etiquetas que se especificaron para el selector de servicio php y el matchLabels del Deployment. Luego agregue app:php y tier:backend bajo template.metadata.labels:

Note: Un pod puede tener múltiples contenedores o volúmenes y cada uno de ellos necesitará un nombre diferente para poder diferenciarlos.  Puede especificar una ruta de montaje para cada volumen para montar selectivamente ese volumen en un contenedor.

First, you need to specify all the volumes that your containers will access. Name this volume code as you had created a PVC named code to hold your application code:

A continuación, especifique el nombre del contenedor junto con la imagen que desea ejecutar dentro de su pod. Hay varias imágenes disponibles en la tienda de Docker (https://hub.docker.com/explore/), pero en este tutorial, utilizaremos la imagen php:7-fpm:

Ahora, monte los volúmenes a los que el contenedor requiere acceso.  Dado que este contenedor ejecutará su código php, necesitará acceso al volumen code creado en el paso anterior. En este paso, también aprenderá cómo copiar el código de su aplicación utilizando un Init Container.

Note: Puede usar un solo initContainer para ejecutar un script que compile su aplicación, o puede usar un initContainer por comando, dependiendo de la complejidad de su proceso de configuración. Debe asegurarse de que los volúmenes estén montados en el initContainer.

To download the code, this tutorial will guide you on how to use a single Init Container with busybox. Busybox is a small container with wget utility that you will use to achieve this.

First, add your initContainer under spec.template.spec y especifique la imagen de busybox:

Luego, para descargar el código en el volumen code, su Init Container necesitará acceso a él. Monte el volumen code en la ruta /code bajo spec.template.spec.initContainers:

Cada Init Container requiere ejecutar un comando. Este Init Container utilizará wget para descargar el code desde Github en el directorio /code. Puede pasar una opción -O para darle un nombre a este archivo descargado, y puede nombrar este archivo index.php.

Note: Asegúrese de confiar en el código que está extrayendo en su servidor utilizando el Init Container. Puede inspeccionar el código fuente y asegurarse de estar de acuerdo con lo que hace.

Además, agregue las siguientes líneas debajo de install container en spec.template.spec.initContainers:

Después de completar todos estos pasos, su php_deployment.yaml archivo debería verse así:

Ahora puede guardar el archivo y salir. A continuación, cree su Deployment de PHP-FPM utilizando el kubectl apply comando:

La creación exitosa del Deployment debería darle la siguiente salida:

Este Deployment comienza descargando las imágenes especificadas, luego solicitará el PersistentVolume de su PersistentVolumeClaim, y luego ejecutará sus initContainers. Una vez hecho este paso, los contenedores se ejecutarán y montarán los volúmenes en el punto de montaje especificado. Después de completar todos estos pasos, su pod estará en funcionamiento.

Puede ejecutar el siguiente comando para ver su Deployment:

Después de ejecutar el comando anterior, debería obtener la siguiente salida:

Puede comprender el estado actual del Deployment con la ayuda de esta salida. Un Deployment es un controlador que mantiene el estado deseado. El DESIRED campo especifica que tiene 1 réplica del pod llamado php. El CURRENT campo indica cuántas réplicas del DESIRED estado se están ejecutando actualmente. Para un pod saludable, esto debería coincidir con el DESIRED estado. Puede obtener más información sobre los campos restantes en la documentación.

Después de eso, para verificar el estado de su pod en ejecución, puede ejecutar el siguiente comando:

La salida de este comando puede variar según el tiempo transcurrido desde que creó su Deployment. Si se ejecuta poco después de crear el Deployment, la salida será similar a:

Explicación:

Estas columnas representan la información de la siguiente manera:

  • Ready: El número de réplicas actuales/deseadas que ejecutan este pod.
  • Estado: El estado de su pod. Init:0/1 indica que los Init Containers se están ejecutando y 0 de 1 Init Containers han terminado de ejecutarse.
  • Reinicios: Esto indica la cantidad de veces que este proceso se ha reiniciado para iniciar el pod.

Su pod puede tardar unos minutos en cambiar el estado a podInitializing dependiendo de la complejidad de sus scripts de inicio:

Esto indica que los Init Containers se han ejecutado correctamente y ahora los contenedores se están inicializando:

Como puede ver ahora, su pod está activo y en funcionamiento. Sin embargo, en caso de que su pod no se inicie, puede ejecutar los siguientes comandos para fines de depuración:

1. Para ver información detallada del pod:

2.  Para ver los logs del pod:

Nota: Hay múltiples opciones disponibles para el comando “kubectl logs”, puede ejecutar el comando “kubectl logs –help” para explorar más al respecto.

3. Para ver los logs de un contenedor específico en el pod:

¡Felicitaciones! Ha montado con éxito el código de la aplicación y el servicio PHP-FPM está listo para manejar conexiones. De manera similar, puede crear su Deployment de Nginx.

Paso 5: Crear su Deployment de Nginx

Este paso le guiará sobre cómo configurar Nginx utilizando un ConfigMap. Un ConfigMap guarda todas las configuraciones requeridas en un formato de clave-valor que se utilizará en otras definiciones de objetos de Kubernetes. Con este enfoque, tendrá la flexibilidad de reutilizar o cambiar la imagen de Nginx por una versión diferente, cuando sea necesario. Puede actualizar el ConfigMap y este replicará automáticamente esos cambios en cualquier pod que esté montando este ConfigMap.

Para empezar, abra un archivo nginx_configmap.yaml en su editor:

Ahora, nombre este ConfigMap como nginx-config y agréguelo al microservicio tier: backend:

Además, puede agregar los datos al ConfigMap. Agregue una clave llamada config y agregue todo el contenido del archivo de configuración de Nginx como valor.

Dado que es posible que Kubernetes dirija las solicitudes a los hosts respectivos para un servicio, puede ingresar el nombre de su servicio PHP-FPM bajo el parámetro fastcgi_pass en lugar de su dirección IP. Agregue las siguientes líneas de código a su archivo nginx_configMap.yaml:

Once completed, your nginx_configMap.yaml file will look like this:

Ahora puede guardar y salir del editor. Ahora ejecute el kubectl apply comando para crear el ConfigMap:

Después de eso, debería ver la siguiente salida en su pantalla:

Ha creado con éxito su Configmap de Nginx. Ahora puede crear su Deployment de Nginx.

Creación del Deployment de Nginx

Para empezar, puede crear un nuevo archivo llamado nginx_deployment.yaml en el editor:

Nombre a este Deployment nginx y añada la etiqueta tier: backend a este:

Después de eso, especifique el número de réplicas añadiendo el campo replica en la especificación (spec) del Deployment y añada las etiquetas app: nginx y tier: backend a este:

De manera similar, añada la plantilla del pod. Asegúrese de añadir las mismas etiquetas que había añadido en el selector.matchLabels del Deployment. Puede añadir lo siguiente:

Dé a Nginx acceso al PVC de código que se creó anteriormente añadiendo los siguientes parámetros bajo spec.template.spec.volumes:

Nota: Un pod puede montar el ConfigMap como un volumen. Al especificar el nombre del archivo y la clave, crearemos un archivo con su valor como contenido. Para usar este ConfigMap, establezca la ruta al nombre del archivo que contiene el contenido de la clave. Puede crear un archivo site.conf a partir de la clave config. Añada lo siguiente bajo spec.template.spec.volumes:

Advertencia: El contenido de la clave reemplazará el mountPath del volumen si no se especifica un archivo. En otras palabras, perderá todo el contenido de la carpeta de destino si no se especifica explícitamente una ruta.

Ahora, especifique el nombre, la imagen y el puerto que desea usar en su pod. Aquí, usaremos la imagen nginx:1.7.9 y el puerto 80. Añádalos bajo spec.template.spec sección:

Además, monta el volumen de código en /code ya que tanto Nginx como PHP-FPM necesitarán acceder al archivo en la misma ruta:

La nginx-1.7.9 carga automáticamente cualquier archivo de configuración bajo la carpeta /etc/nginx/conf.d. Ahora, si montamos el volumen de configuración en este directorio, creará /etc/nginx/conf.d/site.conf. Agrega lo siguiente bajo la sección volumeMount sección:

Después de completar todos los pasos anteriores, tu archivo nginx_deployment.yaml debería verse así:

Ahora puedes guardar y salir del archivo y crear el Deployment de Nginx ejecutando el siguiente comando:

Tras la ejecución exitosa del comando, deberías ver la siguiente salida:

Puedes listar todos tus Deployments ejecutando los siguientes comandos:

Ahora deberías ver tanto el Deployment de Nginx como el de PHP-FPM:

kubernetes deployment status

Además, puede ejecutar el siguiente comando para listar los pods que están gestionados por ambos Deployments listados anteriormente:

Verá que ambos pods están activos y en funcionamiento como se muestra a continuación:

kubernetes pod status

Dado que todos sus objetos de Kubernetes están activos en este momento, ahora puede acceder al servicio Nginx en su navegador.

Ejecute el siguiente comando para listar los servicios:

Tome nota de la IP externa de su servicio Nginx:external ip of nginx Deploy a PHP Application on Kubernetes Cluster with Ubuntu 18.04

Ahora, utilizando esta IP externa del servicio Nginx, puede visitar su servidor escribiendo http://your_public_ip en su navegador. Debería poder ver la salida de php_info() que confirma que sus servicios de Kubernetes están activos y en funcionamiento.

Conclusión

En este tutorial, para gestionar sus servicios PHP-FPM y Nginx de forma independiente, ha contenedorizado los dos servicios. Al hacerlo, no solo mejorará la escalabilidad de su proyecto, sino que también utilizará sus recursos de manera eficiente. También ha aprendido cómo crear almacenamiento local y un volumen persistente (Persistent Volume) para almacenar el código de su aplicación en un volumen y poder actualizar fácilmente sus servicios en el futuro. Al hacerlo, ha mejorado la usabilidad y el mantenimiento de su código.

Además, eche un vistazo a nuestros otros tutoriales centrados en Docker y Kubernetes que puede encontrar en nuestro blog:

¡Feliz computación!

author

Hark Labs

Autor · CloudSigma

Preslav Dobrev es diseñador creativo en CloudSigma, centrado en una identidad empresarial coherente mediante el uso de canales de marketing tradicionales e innovadores. Es experto en fusionar la visión artística con el marketing estratégico para crear narrativas de marca impactantes.

Comentarios

Aún no hay comentarios. Sea el primero.