Diferencia entre revisiones de «Tutorial configurando vagrant para una aplicación 2526»

De Wiki de EGC
Saltar a: navegación, buscar
(Crear el script de aprovisionamiento)
(Estructura del Proyecto)
 
(No se muestran 5 ediciones intermedias de otro usuario)
Línea 45: Línea 45:
 
│  ├── app.py           
 
│  ├── app.py           
 
│  ├── models.py           
 
│  ├── models.py           
│  └── routes.py          
+
│  ├── routes.py
└── templates/
+
│  └── templates/
     └── tasks.html    
+
└──     └── tasks.html
 
  </pre>
 
  </pre>
  
Línea 394: Línea 394:
  
 
</syntaxhighlight>
 
</syntaxhighlight>
 
 
  
 
= Parte 1: Aprovisionamiento mediante un Script de Shell =
 
= Parte 1: Aprovisionamiento mediante un Script de Shell =
Línea 674: Línea 672:
  
 
'''¿Has podido ver qué ocurría en cada paso? ¡Prueba a entrar en la máquina virtual y ver cómo está funcionando todo por dentro!'''
 
'''¿Has podido ver qué ocurría en cada paso? ¡Prueba a entrar en la máquina virtual y ver cómo está funcionando todo por dentro!'''
 +
 +
 +
= Parte 2: Aprovisionamiento mediante un playbook de Ansible =
 +
 +
El aprovisionamiento con Ansible ofrece varias ventajas frente al uso de scripts de shell. Mientras que los scripts de shell son imperativos, es decir, detallan los pasos exactos para lograr un objetivo, Ansible adopta un enfoque '''declarativo''', donde especificamos el estado deseado del sistema y Ansible se encarga de llevarlo a cabo. Esto permite una mayor '''idempotencia''', es decir, que la configuración se pueda aplicar varias veces sin causar efectos no deseados. Además, Ansible facilita la gestión de infraestructuras grandes de forma '''escalable y reproducible''', lo que hace que sea más fácil mantener entornos consistentes y automatizados en comparación con los scripts tradicionales. Un aspecto clave de Ansible es que permite aprovisionar diferentes máquinas sin importar el sistema operativo o la infraestructura subyacente, ya que simplemente le indicamos qué necesitamos y Ansible se encarga de implementar la configuración adecuada, independientemente de cómo o dónde se ejecute.
 +
 +
 +
== Paso 1: Modificar el Vagrantfile ==
 +
 +
Modifica el Vagrantfile para ejecutar un playbook de Ansible en lugar del script provision.sh:
 +
 +
<syntaxhighlight lang="ruby">
 +
# Vagrantfile
 +
# -*- mode: ruby -*-
 +
# vi: set ft=ruby :
 +
 +
Vagrant.configure("2") do |config|
 +
 
 +
  # 1. Sistema Operativo
 +
  config.vm.box = "ubuntu/jammy64"
 +
 
 +
  # 2. Mapear puerto de Flask (usando 5001 como puerto de host para evitar colisiones)
 +
  # Aplicación accesible en http://localhost:5001
 +
  config.vm.network "forwarded_port", guest: 5000, host: 5001
 +
 
 +
  # 3. Aprovisionamiento: Ejecuta el Playbook de Ansible
 +
  config.vm.provision "ansible" do |ansible|
 +
    # Especifica que Vagrant debe ejecutar el archivo playbook.yml
 +
    ansible.playbook = "playbook.yml"
 +
   
 +
    # Ejecutar tareas con privilegios de root (sudo)
 +
    ansible.become = true
 +
   
 +
    # Asegura que se ejecuta en el host de la MV
 +
    ansible.limit = "all"
 +
  end
 +
end
 +
 +
 +
</syntaxhighlight>
 +
 +
== Paso 2: Crea el <code>playbook.yml</code> ==
 +
 +
En la raíz del proyecto, al mismo nivel que el Vagrantfile, crea el playbook.yml.
 +
 +
<syntaxhighlight lang="yaml">
 +
---
 +
- name: Configurar Entorno Flask y MariaDB
 +
  hosts: all
 +
  become: yes # Ejecutar tareas con privilegios de root (sudo)
 +
  vars:
 +
    project_dir: /vagrant
 +
    env_file: "{{ project_dir }}/.env"
 +
 +
  tasks:
 +
    # --- 1. Saneamiento y Carga de Variables de Entorno (.env) ---
 +
    - name: Cargar variables de entorno como JSON y establecer hechos
 +
      ansible.builtin.shell: |
 +
        # 1. Leer el archivo .env, limpiar CRLF (\r) y líneas vacías/comentarios.
 +
        # 2. Convertir el formato KEY=VALUE al formato JSON "KEY":"VALUE".
 +
        CLEAN_ENV=$(
 +
          cat {{ env_file }} |
 +
          tr -d '\r' |
 +
          grep -vE '^\s*#' |
 +
          grep -vE '^\s*$' |
 +
          awk -F'=' '{ gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print "\""$1"\":\""$2"\"" }' |
 +
          paste -s -d ',' -
 +
        )
 +
       
 +
        # 3. Imprimir el JSON completo. Ejemplo: {"KEY1":"VALUE1","KEY1":"VALUE2"}
 +
        echo "{ $CLEAN_ENV }"
 +
      register: env_output
 +
      changed_when: false
 +
      tags: [config, env]
 +
 +
    - name: Establecer variables de entorno limpias como hechos de Ansible
 +
      ansible.builtin.set_fact:
 +
        env_vars: "{{ env_output.stdout | from_json }}"
 +
      tags: [config, env]
 +
     
 +
    # --- 2. Preparación del Sistema ---
 +
    - name: Forzar la actualización del índice de paquetes (apt update)
 +
      ansible.builtin.apt:
 +
        update_cache: yes
 +
      tags: [install, base]
 +
 +
    - name: Instalar MariaDB, Python y utilidades necesarias
 +
      ansible.builtin.package:
 +
        name:
 +
          - mariadb-server
 +
          - python3-pip
 +
          - git
 +
          - python3-dev
 +
          - python3-venv
 +
        state: present
 +
      tags: [install, base]
 +
 +
    - name: Instalar PyMySQL a nivel de sistema (Necesario para los módulos 'mysql_*' de Ansible)
 +
      ansible.builtin.pip:
 +
        name: pymysql
 +
        executable: pip3
 +
      tags: [install, db]
 +
 +
    # --- 3. Configuración de MariaDB (Módulos declarativos de MySQL) ---
 +
    - name: Asegurar que el servicio MariaDB esté iniciado y habilitado
 +
      ansible.builtin.service:
 +
        name: mariadb
 +
        state: started
 +
        enabled: yes
 +
      tags: [db]
 +
 +
    - name: Crear Base de Datos y Usuario
 +
      block:
 +
        - name: Crear Base de Datos
 +
          community.mysql.mysql_db:
 +
            name: "{{ env_vars.DATABASE_DB }}"
 +
            state: present
 +
            encoding: utf8mb4
 +
            collation: utf8mb4_unicode_ci
 +
            login_unix_socket: /run/mysqld/mysqld.sock
 +
          tags: [db]
 +
 +
        - name: Crear Usuario de Aplicación y Otorgar Permisos
 +
          community.mysql.mysql_user:
 +
            name: "{{ env_vars.DATABASE_USER }}"
 +
            password: "{{ env_vars.DATABASE_PASSWORD }}"
 +
            host: "localhost"
 +
            priv: "{{ env_vars.DATABASE_DB }}.*:ALL"
 +
            state: present
 +
            login_unix_socket: /run/mysqld/mysqld.sock
 +
          tags: [db]
 +
 +
    # --- 4. Configuración de la Aplicación Flask ---
 +
    - name: Instalar dependencias de Python a nivel de sistema
 +
      ansible.builtin.pip:
 +
        requirements: "{{ project_dir }}/requirements.txt"
 +
        executable: pip3 # Usamos pip3 del sistema
 +
        state: present
 +
      tags: [app, install]
 +
 +
    - name: Aplicar migraciones de base de datos (Flask-Migrate)
 +
      ansible.builtin.command: python3 -m flask db upgrade # Usamos python3 -m flask
 +
      environment:
 +
        DATABASE_USER: "{{ env_vars.DATABASE_USER }}"
 +
        DATABASE_PASSWORD: "{{ env_vars.DATABASE_PASSWORD }}"
 +
        DATABASE_DB: "{{ env_vars.DATABASE_DB }}"
 +
        DATABASE_HOST: "{{ env_vars.DATABASE_HOST | default('localhost') }}"
 +
        FLASK_APP: app.app:create_app
 +
      args:
 +
        chdir: "{{ project_dir }}"
 +
      become: yes
 +
      become_user: vagrant
 +
      tags: [app, db]
 +
 +
    # --- 5. Lanzar la Aplicación ---
 +
   
 +
    # Tarea 5.1: Detener procesos previos
 +
    - name: Detener instancias de Flask previamente en ejecución
 +
      ansible.builtin.shell: "pkill -f 'python3 -m flask run --host=0.0.0.0'"
 +
      become: yes
 +
      become_user: vagrant
 +
      failed_when: false # CRÍTICO: Ignorar el error si pkill no encuentra nada.
 +
      tags: [app, run]
 +
 +
    # Tarea 5.2: Iniciar Flask en segundo plano con nohup
 +
    - name: Iniciar Flask en segundo plano con nohup
 +
      ansible.builtin.shell: |
 +
        nohup python3 -m flask run --host=0.0.0.0 > /tmp/flask_app.log 2>&1 &
 +
      environment:
 +
        FLASK_APP: app.app:create_app
 +
        DATABASE_USER: "{{ env_vars.DATABASE_USER }}"
 +
        DATABASE_PASSWORD: "{{ env_vars.DATABASE_PASSWORD }}"
 +
        DATABASE_DB: "{{ env_vars.DATABASE_DB }}"
 +
        DATABASE_HOST: "{{ env_vars.DATABASE_HOST | default('localhost') }}"
 +
      args:
 +
        chdir: "{{ project_dir }}"
 +
      become: yes
 +
      become_user: vagrant
 +
      tags: [app, run]
 +
 +
    - name: Verificar que Flask se esté ejecutando
 +
      ansible.builtin.shell: "ps aux | grep -v grep | grep 'python3 -m flask run --host=0.0.0.0'"
 +
      register: flask_status
 +
      failed_when: flask_status.rc != 0 and 'python3 -m flask run' not in flask_status.stdout
 +
      tags: [app, run]
 +
 +
    - name: Mostrar mensaje de acceso
 +
      ansible.builtin.debug:
 +
        msg: "✅ ¡Aprovisionamiento COMPLETO! La aplicación Flask está en segundo plano. Acceda en http://localhost:5001"
 +
      tags: [app, run]
 +
 +
 +
</syntaxhighlight>
 +
 +
 +
=== Paso 2: Explicación detallada del <code>playbook.yml</code> ===
 +
 +
==== Carga y Saneamiento de Variables ====
 +
 +
Este bloque es fundamental para la estabilidad. Resuelve los problemas de caracteres invisibles del archivo <code>.env</code> que suelen causar fallos en MariaDB, cargando las variables en un formato JSON limpio.
 +
 +
Ansible lee el archivo <code>/vagrant/.env</code>, utiliza herramientas de shell como <code>tr</code> y <code>awk</code> para eliminar cualquier carácter de retorno de carro (CRLF) o espacio sobrante, y lo formatea como un objeto JSON.
 +
 +
El módulo <code>set_fact</code> carga este JSON limpio en la variable <code>env_vars</code>, garantizando que nombres como la base de datos y el usuario sean cadenas de texto perfectas.
 +
 +
==== Instalación de Componentes Base ====
 +
 +
Esta sección configura el sistema operativo para ejecutar la base de datos y la aplicación Python.
 +
 +
<code>sudo apt update</code>: Se fuerza la actualización del índice de paquetes para asegurar que se instalen las versiones más recientes.
 +
 +
<code>sudo apt install mariadb-server python3-pip python3-dev ...</code>: Instala todos los paquetes necesarios del sistema: el servidor de MariaDB, las herramientas de Python 3, y las cabeceras de desarrollo de Python (<code>python3-dev</code>), que son necesarias para compilar bibliotecas binarias como mysqlclient.
 +
 +
<code>pip3 install pymysql</code>: Instala la biblioteca PyMySQL a nivel de sistema. Esta biblioteca no es para la aplicación Flask, sino para que Ansible pueda comunicarse con MariaDB y crear la base de datos y el usuario en las siguientes tareas.
 +
 +
==== Configuración de MariaDB ====
 +
 +
Este bloque configura la base de datos y el usuario de manera idempotente utilizando los módulos declarativos de Ansible, que reemplazan la necesidad de ejecutar comandos SQL manuales.
 +
 +
La tarea <code>Asegurar que el servicio MariaDB esté iniciado y habilitado</code> equivale a:
 +
<code>sudo systemctl enable mariadb / sudo systemctl start mariadb</code>: Asegura que el servicio MariaDB esté habilitado para arrancar con el sistema y lo inicia inmediatamente.
 +
 +
La tarea <code>Crear Base de Datos</code> utiliza el valor limpio de <code>DATABASE_DB</code> para crearla con la codificación correcta (utf8mb4).
 +
 +
La tarea <code>Crear Usuario de Aplicación y Otorgar Permisos</code> utiliza los valores limpios de <code>DATABASE_USER</code> y <code>DATABASE_PASSWORD</code> para crear el usuario y otorgarle todos los permisos (<code>GRANT ALL PRIVILEGES</code>) sobre la base de datos recién creada.
 +
 +
==== Instalación de Dependencias Python  ====
 +
 +
La tarea <code>Instalar dependencias de Python a nivel de sistema</code> equivale a:
 +
<code>sudo pip3 install -r /vagrant/requirements.txt</code>: Instala todas las dependencias listadas en el archivo <code>requirements.txt</code> a nivel global del sistema.
 +
 +
==== Migraciones y Creación de Tablas ====
 +
 +
Se utiliza Flask-Migrate para configurar la base de datos con el esquema de modelos de SQLAlchemy.
 +
 +
La tarea <code>Aplicar migraciones de base de datos (Flask-Migrate)</code> ejecuta el comando: <code>export FLASK_APP=app.app:create_app</code>
 +
 +
<code>python3 -m flask db upgrade</code>: Aplica la migración más reciente a la base de datos, creando todas las tablas definidas por los modelos de la aplicación (o actualizándolas si ya existen). Esta tarea se ejecuta como el usuario <code>vagrant</code> para asegurar los permisos de acceso a los archivos del proyecto.
 +
 +
==== Lanzamiento de la Aplicación ====
 +
 +
Finalmente, se lanza la aplicación Flask y se asegura su continuidad.
 +
 +
La tarea <code>Detener instancias de Flask previamente en ejecución</code> ejecuta:
 +
<code>pkill -f 'python3 -m flask run --host=0.0.0.0'</code>: Intenta detener cualquier proceso Flask anterior. Es crucial que la tarea tenga la opción <code>failed_when: false</code>, para que Ansible no falle si no se encuentra ningún proceso que matar.
 +
 +
La tarea <code>Iniciar Flask en segundo plano con nohup</code> ejecuta:
 +
<code>nohup python3 -m flask run --host=0.0.0.0 > /tmp/flask_app.log 2>&1 &</code>: Lanza el servidor Flask:
 +
 +
<code>--host=0.0.0.0</code>: Permite que la aplicación sea accesible desde fuera de la VM (a través del puerto reenviado en Vagrantfile).
 +
 +
<code>nohup ... &</code>: Ejecuta el comando en segundo plano (<code>&</code>) y lo hace inmune a colgarse (<code>nohup</code>), manteniendo el servidor activo. La salida se redirige al archivo <code>/tmp/flask_app.log</code>.
 +
 +
Verificación y Mensaje Final: Las últimas tareas verifican que el proceso Flask esté corriendo y muestran un mensaje de éxito, indicando que la aplicación está lista en <code>http://localhost:5001</code>.
 +
 +
La aplicación Flask estará disponible en `http://localhost:5001` cuando finalice el aprovisionamiento.
 +
 +
'''¿Qué has podido observar ahora en la terminal en la etapa de aprovisionamiento? ¿Crees que la MV estará funcionando igual que en el aprovisionamiento por shell script?'''
 +
 +
=Conclusión=
 +
 +
Este tutorial te ha guiado a través de dos métodos de aprovisionamiento de una aplicación Flask en Vagrant, utilizando un script de shell y un playbook de Ansible. ¡Ahora deberías tener una mejor comprensión del aprovisionamiento automático y de cómo desplegar aplicaciones en entornos virtuales reproducibles!
 +
 +
¡Enhorabuena si has llegado hasta aquí! Y, recuerda, las máquinas virtuales consumen recursos de tu equipo, apágalas y elimínalas cuando ya no las necesites.

Revisión actual del 09:47 18 nov 2025

Contenido

Despliegue de una Aplicación Python en Vagrant

Introducción

Este tutorial te guiará en el despliegue de una aplicación Flask utilizando Vagrant, Ansible y Shell scripts. La finalidad es entender cómo automatizar la configuración y despliegue de aplicaciones en entornos virtuales reproducibles, como los que proporciona Vagrant.

¿Por qué utilizamos Vagrant?

Vagrant es una herramienta de administración de entornos virtuales que permite definir en un archivo (llamado `Vagrantfile`) las especificaciones necesarias para crear, configurar y aprovisionar una máquina virtual de manera rápida y consistente. Vagrant es especialmente útil en desarrollo y pruebas, ya que nos permite configurar un entorno de desarrollo idéntico al entorno de producción en cuestión de minutos y garantiza que cada miembro del equipo de desarrollo trabaje en un entorno coherente.

Con Vagrant, evitamos el clásico problema de "en mi máquina funciona" al tener un entorno idéntico en cada ejecución, independientemente de la máquina en la que se esté corriendo el proyecto. En este tutorial, usaremos Vagrant para crear una máquina virtual de Linux donde desplegaremos nuestra aplicación de Flask.

¿Qué es el Aprovisionamiento?

Aprovisionar, en el contexto de máquinas virtuales y Vagrant, se refiere al proceso de instalar y configurar el software necesario en una máquina virtual para que pueda ejecutar una aplicación. Cuando aprovisionamos una máquina, estamos automatizando la instalación de dependencias, configuraciones de sistema, servicios necesarios (como bases de datos), y otras tareas de configuración que preparan el entorno para ejecutar la aplicación.

Métodos de Aprovisionamiento

En este tutorial veremos dos métodos de aprovisionamiento:

1. Mediante un script de shell: Usando un archivo `.sh` (como `provision.sh`), que contiene comandos de terminal para instalar y configurar los componentes necesarios.

2. Mediante Ansible: Usando un archivo de configuración llamado `playbook.yml`, que describe de manera declarativa lo que queremos instalar y configurar en la máquina virtual.

¿Por qué utilizar Ansible para el Aprovisionamiento?

Ansible es una herramienta de automatización de TI que permite gestionar la configuración, el despliegue y la orquestación de sistemas. Comparado con los scripts de shell, Ansible ofrece varias ventajas:

  • Declarativo y legible: Un playbook de Ansible describe el estado deseado del sistema (qué queremos conseguir), en lugar de los pasos para llegar a ese estado (cómo hacerlo), lo cual hace que sea más legible y fácil de mantener.
  • Idempotencia: Ansible ejecuta las tareas de manera idempotente, lo que significa que si ejecutamos el playbook varias veces, el sistema solo aplicará los cambios necesarios, evitando que se realicen operaciones innecesarias.
  • Escalabilidad y organización: Ansible permite crear playbooks para gestionar múltiples servidores de manera centralizada, lo cual es esencial en ambientes de producción donde hay decenas o cientos de servidores.
  • Ecosistema y comunidad: Ansible es una herramienta popular con una gran comunidad de usuarios y módulos preexistentes para una amplia variedad de tareas, lo que facilita extender la funcionalidad y resolver problemas comunes rápidamente.

¿Cuándo es mejor utilizar un script de shell y cuándo Ansible?

Para proyectos pequeños o configuraciones rápidas, un script de shell puede ser suficiente. Sin embargo, para entornos más complejos, donde la repetición, escalabilidad y legibilidad son factores importantes, Ansible es preferible debido a su idempotencia, claridad y modularidad.

En este tutorial, vamos a provisionar un entorno para nuestra aplicación Flask de ambas maneras, para que puedas comparar los métodos y comprender en qué casos uno puede ser más ventajoso que el otro.

Nuestra Aplicación: Gestión de Tareas

La aplicación que desplegaremos es una sencilla aplicación de gestión de tareas, utilizada previamente en otras prácticas. Esta aplicación está basada en Flask, un framework de desarrollo web en Python, y ahora utilizará una base de datos MariaDB para almacenar información. El propósito de este tutorial es aprender a desplegar la aplicación en un entorno controlado y reproducible usando Vagrant y los métodos de aprovisionamiento mencionados anteriormente.

Estructura del Proyecto

Para esta práctica, el proyecto debería presentar la siguiente estructura de archivos:

flask_testing_project/
├── .env                                      
├── config.py               
├── app/                   
│   ├── __init__.py
│   ├── app.py          
│   ├── models.py          
│   ├── routes.py
│   └── templates/
└──     └── tasks.html
 

Y el siguiente contenido:

Código .env:

El archivo .env es un estándar para almacenar configuraciones que varían entre entornos (desarrollo, pruebas, producción) y que no deben ser hardcodeadas o subidas a repositorios de código (como contraseñas). El paquete python-dotenv cargará estas variables en el entorno de la aplicación al inicio.

Finalidad: Centralizar todas las credenciales de MariaDB y los secretos de Flask, facilitando que tanto el script de aprovisionamiento de Vagrant como la aplicación Flask accedan a ellas.

# .env file (Usado en Local y en Vagrant)

# --- 1. Credenciales de la Base de Datos (Para la aplicación Flask) ---
DATABASE_USER=flask_user
DATABASE_PASSWORD=flask_password
DATABASE_HOST=localhost
DATABASE_DB=tasks_db

# --- 2. Variables de la Aplicación Flask ---
SECRET_KEY=una-clave-secreta-larga-y-unica

# --- 3. Credenciales de MariaDB Root ---
MYSQL_ROOT_PASSWORD=su_contraseña_root_segura
Código config.py:

Este módulo define clases de configuración que heredan de una base (Config) y permiten aplicar ajustes específicos para diferentes entornos (ej. DEBUG = True solo en DevelopmentConfig).

Finalidad: Construir la cadena de conexión de la base de datos (SQLALCHEMY_DATABASE_URI) a partir de las variables individuales leídas del entorno (os.environ.get(...)), asegurando que la aplicación sepa cómo conectarse a MariaDB.

import os

# Configuración base (compartida entre entornos)
class Config:
	# Lee SECRET_KEY del .env
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'fallback-clave-secreta' 
    
    # Lee las variables individuales de la DB
    DB_USER = os.environ.get('DATABASE_USER')
    DB_PASS = os.environ.get('DATABASE_PASSWORD')
    DB_HOST = os.environ.get('DATABASE_HOST')
    DB_NAME = os.environ.get('DATABASE_DB')

    # Construye la URL de conexión
    SQLALCHEMY_DATABASE_URI = (
        f'mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}/{DB_NAME}' 
        if DB_USER and DB_PASS and DB_HOST and DB_NAME else None
    )
    
    SQLALCHEMY_TRACK_MODIFICATIONS = False

# Configuración específica para desarrollo
class DevelopmentConfig(Config):
    DEBUG = True
Código __init__.py:

Este archivo es el módulo de inicialización del paquete Python app. Contiene una única línea de código que expone la función principal del paquete.

Finalidad: Actúa como un proxy o puente para que cualquier otro archivo que necesite acceder a la función fábrica de la aplicación (como el comando flask o scripts externos) pueda importarla directamente usando from app import create_app, sin tener que especificar el archivo interno app.app. Simplemente hace que la función create_app definida en app.py sea accesible al nivel superior del paquete app.

from .app import create_app
Código app.py:

Este módulo contiene la función fábrica de la aplicación (create_app), el corazón de la arquitectura Flask. Es responsable de crear y configurar la instancia de la aplicación de manera flexible, permitiendo diferentes configuraciones (como Desarrollo o Pruebas).

Finalidad: La función create_app es el punto de control central que:

1) Carga el Entorno: Usa load_dotenv() para cargar variables de configuración (credenciales de DB, SECRET_KEY) desde el archivo .env.

2) Configura la Aplicación: Aplica los ajustes específicos de entorno (tomados de config.py) a la instancia de Flask.

3) Inicializa Extensiones: Vincula extensiones como SQLAlchemy (db.init_app) y Flask-Migrate a la aplicación.

4) Registra Rutas: Agrega los módulos de rutas (los Blueprints) a la aplicación, completando la configuración antes de ser devuelta.


from flask import Flask
from app.routes import bp as tasks_blueprint
from .models import db # SQLAlchemy instance
from flask_migrate import Migrate
from config import DevelopmentConfig # Importamos la configuración
from dotenv import load_dotenv
import os

load_dotenv()


def create_app(config_class=DevelopmentConfig): # Usa Desarrollo por defecto
    app = Flask(__name__)
    
    # 1. Aplicar la configuración
    app.config.from_object(config_class)
    
    # 2. Inicializar extensiones
    db.init_app(app)
    # Inicializar Flask-Migrate (necesita app y db)
    Migrate(app, db) 

    app.register_blueprint(tasks_blueprint)
        
    return app
Código models.py:

Este módulo define el Modelo Relacional de Objetos utilizando Flask-SQLAlchemy (ORM).

Finalidad: Declarar la estructura de la tabla tasks y proporcionar funciones de acceso a datos de alto nivel (get_all_tasks, create_task) que interactúan con la base de datos de manera segura dentro de un contexto de aplicación (current_app.app_context()).

from flask_sqlalchemy import SQLAlchemy
from flask import current_app

db = SQLAlchemy()

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    done = db.Column(db.Boolean, default=False)
    
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'done': self.done
        }

# --- Funciones de Acceso a Datos ---

def get_all_tasks():
    with current_app.app_context():
        return [task.to_dict() for task in Task.query.order_by(Task.id).all()]

def create_task(title):
    if not title:
        raise ValueError("El título es necesario")
    
    with current_app.app_context():
        new_task = Task(title=title, done=False)
        db.session.add(new_task)
        db.session.commit()
        return new_task.to_dict()


Código routes.py:

Este módulo utiliza el concepto de Blueprint de Flask para organizar las rutas y vistas de la aplicación, separando la lógica de enrutamiento de la lógica de la fábrica.

Finalidad: Define los endpoints HTTP (/, /add_task, /tasks). Incluye rutas para la interfaz web (render_template) y endpoints API (jsonify) que llaman a las funciones de acceso a datos definidas en models.py.

from flask import Blueprint, jsonify, request, render_template, redirect, url_for
from app.models import get_all_tasks, create_task

bp = Blueprint('tasks', __name__)

@bp.route('/')
def task_list():
    return render_template('tasks.html', tasks=get_all_tasks())

@bp.route('/tasks', methods=['GET'])
def get_tasks():
    return jsonify({'tasks': get_all_tasks()})

@bp.route('/add_task', methods=['POST'])
def add_task_html():
    title = request.form.get('title')
    try:
        create_task(title)
        return redirect(url_for('tasks.task_list'))
    except ValueError as e:
        return str(e), 400

@bp.route('/tasks', methods=['POST'])
def create_task_api():
    data = request.get_json()
    title = data.get('title') if data else None
    try:
        task = create_task(title)
        return jsonify(task), 201
    except ValueError as e:
        return jsonify({'error': str(e)}), 400


Archivo templates/tasks.html:

La plantilla tasks.html es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gestor de Tareas</title>
</head>
<body>
    <h1>Gestor de Tareas</h1>

    <form action="{{ url_for('tasks.add_task_html') }}" method="POST">
        <input type="text" name="title" placeholder="Añadir nueva tarea">
        <button type="submit">Añadir tarea</button>
    </form>

    <h2>Lista de Tareas:</h2>
    <ul>
        {% for task in tasks %}
            <li>{{ task.title }} {% if task.done %}(completada){% endif %}</li>
        {% endfor %}
    </ul>
</body>
</html>
Script local_setup.sh para lanzar la app en local

Si quieres lanzar la aplicación de manera local, puedes utilizar este script:

#!/bin/bash

# =========================================================================
# SCRIPT DE SETUP Y LANZAMIENTO LOCAL PARA FLASK CON MARIADB
# =========================================================================

# --- 0. Asegurar la ubicación ---
# Cambia al directorio del script. Esto garantiza que encuentre .env y .venv.
cd "$(dirname "$0")"
echo "Directorio de trabajo actual: $(pwd)"

# --- 1. Variables y Rutas del Entorno Virtual ---
# Definimos las rutas a los binarios DENTRO del venv para usar rutas ABSOLUTAS.
VENV_DIR=".venv"
PYTHON_BIN="$VENV_DIR/bin/python"
PIP_BIN="$VENV_DIR/bin/pip"
FLASK_BIN="$VENV_DIR/bin/flask"

# --- 2. Cargar Variables de Configuración desde .env ---
echo "--- 1. Cargando configuración desde el archivo .env ---"

if [ ! -f .env ]; then
    echo "❌ ERROR: El archivo .env no se encontró en $(pwd). ¡Crealo primero!"
    exit 1
fi

. .env

# Comprobación de carga
if [ -z "$DATABASE_DB" ]; then
    echo "❌ ERROR: Las variables de la base de datos (ej. DATABASE_DB) no se cargaron correctamente."
    exit 1
fi
echo "✅ Variables cargadas: Usuario=$DATABASE_USER, DB=$DATABASE_DB"

# --- 3. Preparar Entorno Python e Instalar Dependencias ---
echo "--- 2. Creando Entorno Virtual Python (.venv) ---"

# Crear el entorno virtual si no existe
if [ ! -d "$VENV_DIR" ]; then
    python3 -m venv "$VENV_DIR"
fi

# Usando el PIP ABSOLUTO del entorno virtual para la instalación
echo "--- Instalando dependencias necesarias (Flask, SQLAlchemy, PyMySQL, etc.) ---"
"$PIP_BIN" install --upgrade pip
# Lista de dependencias clave para el proyecto:
"$PIP_BIN" install flask python-dotenv flask-sqlalchemy pymysql flask-migrate

# --- 4. Generar requirements.txt ---
echo "--- 3. Generando requirements.txt a partir del entorno actual ---"
"$PIP_BIN" freeze > requirements.txt
echo "✅ requirements.txt creado con las dependencias instaladas."

# --- 5. Configuración de MariaDB (Usuario y Base de Datos) ---
echo "--- 4. Configurando usuario y base de datos en MariaDB... ---"

# Se ejecuta el comando SQL con las variables cargadas
# Esto requiere permisos de sudo para acceder al usuario 'root' de MariaDB
sudo mysql -u root <<EOF
CREATE DATABASE IF NOT EXISTS $DATABASE_DB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '$DATABASE_USER'@'localhost' IDENTIFIED BY '$DATABASE_PASSWORD';
GRANT ALL PRIVILEGES ON $DATABASE_DB.* TO '$DATABASE_USER'@'localhost';
FLUSH PRIVILEGES;
EOF

echo "✅ Usuario '$DATABASE_USER' y base de datos '$DATABASE_DB' configurados."

# --- 6. Inicializar y Aplicar Migraciones ---
echo "--- 5. Aplicando migraciones de base de datos (Flask-Migrate)... ---"

# Exporta la variable FLASK_APP
export FLASK_APP=app.app:create_app

# Usando el binario de Flask del VENV para ejecutar la migración
"$FLASK_BIN" db upgrade

echo "✅ Migraciones completadas. Las tablas están listas."

# --- 7. Lanzar la Aplicación ---
echo "--- 6. Lanzando el servidor Flask en http://127.0.0.1:5000 ---"

# Usando el binario de Flask del VENV para ejecutar la aplicación
"$FLASK_BIN" run


Para lanzarlo, usa:

chmod +x local_setup.sh
./local_setup.sh

Parte 1: Aprovisionamiento mediante un Script de Shell

En la primera parte del tutorial, aprovisionaremos el entorno de desarrollo con un script de shell (`provision.sh`).

¿Qué es un archivo con extensión .sh?

Un archivo con extensión .sh es un script de shell, un archivo de texto que contiene una serie de comandos escritos en un lenguaje de scripting específico para el shell, el intérprete de comandos que interactúa con el sistema operativo. Los scripts .sh se usan ampliamente en sistemas Unix y Linux, aunque también funcionan en otros entornos compatibles con shells como bash, sh o zsh.

¿Para qué sirven los archivos .sh?

Los archivos .sh permiten automatizar tareas, ya que el shell ejecuta cada comando en el archivo de forma secuencial, sin necesidad de intervención manual. Esto es especialmente útil para configurar sistemas, instalar paquetes, mover o copiar archivos, configurar redes, iniciar aplicaciones y realizar prácticamente cualquier acción que se pueda ejecutar desde la línea de comandos.

Por ejemplo, en el contexto de nuestro proyecto con Vagrant y Flask, usaremos un archivo .sh llamado provision.sh para:

Instalar dependencias como Python y Flask. Configurar la base de datos. Preparar el entorno de la aplicación. Esto permite que, al ejecutar vagrant up, el script se ejecute automáticamente, configurando la máquina virtual sin necesidad de realizar los pasos de configuración uno a uno.

Cómo se ejecuta un archivo .sh

Para ejecutar un archivo .sh en un sistema Linux o Mac, simplemente hay que abrir una terminal y escribir:
 sh nombre_del_archivo.sh
O bien, puedes ejecutar el script con permisos adicionales o en otro shell específico, como bash:
 bash nombre_del_archivo.sh

Cuando Vagrant encuentra un archivo .sh especificado en el Vagrantfile (como en config.vm.provision "shell", path: "provision.sh"), ejecuta ese archivo automáticamente en la máquina virtual como parte del proceso de aprovisionamiento. Esto garantiza que la máquina esté configurada correctamente cada vez que se inicia.


Crear el Vagrantfile

Recuerda que el Vagrantfile es el archivo de configuración que Vagrant utiliza para definir y gestionar una máquina virtual. Este archivo es el núcleo de cualquier proyecto que utilice Vagrant, ya que contiene instrucciones específicas para la configuración de la máquina virtual, incluyendo el sistema operativo, red, sincronización de carpetas, y scripts de aprovisionamiento.

Al colocar el Vagrantfile en el directorio raíz del proyecto (flask_testing_project/), mantenemos todos los recursos y configuraciones necesarias para el despliegue de la aplicación en un solo lugar. Esto hace que el proyecto sea fácil de portar y reproducir, ya que cualquier persona con este directorio y Vagrant puede ejecutar el proyecto en una máquina virtual idéntica.

A continuación, crea un archivo llamado Vagrantfile en el directorio flask_testing_project/ y añade el siguiente contenido:

# Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  
  # 1. Sistema Operativo
  config.vm.box = "ubuntu/jammy64" 
  
  # 2. Mapear puerto de Flask (5000)
  config.vm.network "forwarded_port", guest: 5000, host: 5000
  
  # 3. Provisionamiento: Ejecuta el script completo
  config.vm.provision "shell", path: "provision.sh"
end

Explicación línea a línea del Vagrantfile

El archivo Vagrantfile define las especificaciones para crear y configurar la máquina virtual (VM) en la que se ejecutará tu aplicación Flask.

Vagrant.configure("2") do |config|

Esta línea inicia el bloque de configuración del entorno de Vagrant. Indica que estamos utilizando la versión 2 del formato de configuración y asigna un objeto llamado config que se usará para definir todos los ajustes de la máquina virtual.

config.vm.box = "ubuntu/jammy64"

Esta es la línea que define el Sistema Operativo de la máquina virtual. "ubuntu/jammy64" es el nombre de la "caja base" (la imagen del sistema operativo) que Vagrant descargará e instalará. En este caso, se especifica una versión de Ubuntu 64 bits (Jammy Jellyfish, o 22.04 LTS).

config.vm.network "forwarded_port", guest: 5000, host: 5000

Esta línea se encarga del Mapeo de Puertos. Le indica a Vagrant que debe redirigir el tráfico. Específicamente, el puerto 5000 de la máquina anfitriona (tu máquina local, el host) se reenvía al puerto 5000 dentro de la máquina virtual (guest). Esto es crucial porque permite que accedas a la aplicación Flask (que se ejecuta dentro de la VM en el puerto 5000) desde tu navegador web local visitando http://localhost:5000.

config.vm.provision "shell", path: "provision.sh"

Esta línea define el aprovisionamiento. Le indica a Vagrant que, después de arrancar e instalar el sistema operativo, debe ejecutar un script de automatización. El tipo de provisioner es "shell", y el argumento path: "provision.sh" especifica que el archivo a ejecutar es provision.sh, el cual debe encontrarse en el mismo directorio que este Vagrantfile. Este script se encargará de instalar MariaDB, Python y configurar la base de datos y la aplicación.

end

Esta línea simplemente cierra el bloque de configuración iniciado por Vagrant.configure("2").


Crear el script de aprovisionamiento

Este script, llamado provision.sh, es ejecutado por Vagrant cuando se levanta la máquina virtual (vagrant up). Su objetivo es automatizar la instalación de software, la configuración de la base de datos y el despliegue inicial de la aplicación Flask.

En la raíz del proyecto, al mismo nivel que el Vagrantfile, crea un script provision.sh con este contenido:

#!/bin/bash

# --- 1. Cargar Variables del .env ---
# Carga las credenciales de la DB y la aplicación desde el archivo unificado .env
echo "--- 1. Cargando configuración desde el archivo .env ---"
set -a
# Usamos el path /vagrant para acceder a los archivos del directorio compartido
source /vagrant/.env 
set +a

PROJECT_DIR="/vagrant"

# --- 2. Instalación de Dependencias del Sistema ---
echo "--- 2. Instalando MariaDB, Python (>=3.10) y utilidades ---"
sudo apt-get update
# python3-dev es necesario para compilar algunas dependencias
sudo apt-get install -y mariadb-server python3-pip python3-venv git python3-dev 

# --- 3. Configuración de MariaDB (Idempotente) ---
echo "--- 3. Configurando MariaDB y creando usuario '$DATABASE_USER' ---"
sudo systemctl enable mariadb
sudo systemctl start mariadb
sleep 5 

# Los comandos SQL deben usar la sintaxis más compatible para asegurar la creación del usuario.
sudo mysql -u root <<EOF
-- 3a. Crear la base de datos
CREATE DATABASE IF NOT EXISTS $DATABASE_DB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 3b. Crear/Modificar el usuario con la contraseña
-- Usamos la sintaxis estándar para compatibilidad.
CREATE USER IF NOT EXISTS '$DATABASE_USER'@'localhost' IDENTIFIED BY '$DATABASE_PASSWORD';

-- 3c. Asegurar que el plugin de autenticación sea compatible con PyMySQL (si la versión de MariaDB lo requiere)
-- Nota: Si usas una versión reciente de MariaDB, este paso es implícito o usa IDENTIFIED BY.
-- Lo mantenemos simple para evitar el ERROR 1064.

-- 3d. Otorgar permisos sobre la base de datos
GRANT ALL PRIVILEGES ON $DATABASE_DB.* TO '$DATABASE_USER'@'localhost';

-- 3e. Aplicar los cambios
FLUSH PRIVILEGES;
EOF

echo "✅ MariaDB configurado."

# --- 4. Preparación del Entorno Python ---
echo "--- 4. Configurando entorno Python e instalando requirements.txt ---"
cd $PROJECT_DIR

# 4a. Crear y activar el entorno virtual
if [ ! -d ".venv" ]; then
    python3 -m venv .venv
fi
source .venv/bin/activate

# 4b. Instalar dependencias
pip install -r requirements.txt

# --- 5. Migraciones y Creación de Tablas ---
echo "--- 5. Ejecutando migraciones de Flask (Alembic) ---"
export FLASK_APP=app.app:create_app

# 5a. Inicializar el repositorio de migraciones (si no existe)
if [ ! -d "migrations" ]; then
    flask db init
fi

# 5b. Crear la migración inicial si es necesario (ESTA ES LA LÍNEA FALTANTE)
# Flask-Migrate solo crea tablas si existe un archivo de versión.
flask db migrate -m "Initial database migration"

# 5c. Aplicar las migraciones. Esto crea las tablas en la base de datos.
flask db upgrade

echo "✅ Base de datos inicializada."

# --- 6. Lanzamiento de la Aplicación ---
echo "--- 6. Iniciando la aplicación Flask en segundo plano (Puerto 5000) ---"

# Detener cualquier instancia previa para idempotencia
pkill -f 'flask run --host=0.0.0.0'

# Iniciar la aplicación en segundo plano con nohup
# El .env se carga automáticamente por python-dotenv
nohup flask run --host=0.0.0.0 > /tmp/flask_app.log 2>&1 &

echo "✅ Aprovisionamiento y lanzamiento completados. Accede en http://localhost:5000"
deactivate


Explicación paso a paso del script de aprovisionamiento

Aquí tienes la explicación detallada del script en texto, agrupada por pasos lógicos:

Paso 1: Carga de Variables de Configuración

Este bloque asegura que las credenciales de la base de datos y otras variables de entorno definidas en el archivo .env de tu máquina local estén disponibles dentro de la máquina virtual (VM).

set -a: Este comando asegura que todas las variables definidas o modificadas a partir de este punto sean automáticamente exportadas al entorno (export all), haciéndolas accesibles para los comandos subsiguientes como mysql.

source /vagrant/.env: Comando clave. El directorio /vagrant es el punto de montaje predeterminado que conecta el directorio de tu proyecto local con la VM. Este comando lee el archivo .env compartido e inserta sus variables ($DATABASE_USER, $DATABASE_DB, etc.) en el shell de la VM.

set +a Desactiva la exportación automática de variables.

PROJECT_DIR="/vagrant": Define una variable para el directorio raíz del proyecto, facilitando la navegación posterior.

Paso 2: Instalación de Dependencias del Sistema

Esta sección se encarga de instalar todo el software que necesita la aplicación para funcionar, asumiendo que la VM comienza con un sistema operativo limpio.

sudo apt-get update: Actualiza la lista de paquetes disponibles en el sistema operativo.

sudo apt-get install -y mariadb-server python3-pip python3-venv git python3-dev: Instala los paquetes esenciales sin pedir confirmación (-y):

  • mariadb-server: El servidor de base de datos.
  • python3-pip y python3-venv: Herramientas necesarias para gestionar paquetes y crear entornos virtuales de Python.
  • git: Útil para gestión de código.
  • python3-dev: Necesario para compilar bibliotecas de Python que tienen componentes nativos, como el conector de MariaDB (pymysql).

Paso 3: Configuración de MariaDB

Este bloque configura la base de datos y el usuario de manera idempotente (es decir, puede ejecutarse varias veces sin causar errores).

sudo systemctl enable mariadb / sudo systemctl start mariadb: Asegura que el servicio MariaDB esté habilitado para arrancar con el sistema y lo inicia inmediatamente.

sleep 5: Espera 5 segundos para que el servicio de la base de datos se inicie completamente antes de intentar ejecutar comandos SQL.

sudo mysql -u root <<EOF ... EOF: Ejecuta los comandos SQL dentro del bloque.

  • CREATE DATABASE IF NOT EXISTS $DATABASE_DB ...: Crea la base de datos utilizando la variable cargada, si esta aún no existe.
  • CREATE USER IF NOT EXISTS '$DATABASE_USER'@'localhost' ...: Crea el usuario de la aplicación, también utilizando las variables cargadas.
  • GRANT ALL PRIVILEGES ON $DATABASE_DB.* TO '$DATABASE_USER'@'localhost': Otorga todos los permisos al usuario recién creado, limitando su acceso a la base de datos de la aplicación.
  • FLUSH PRIVILEGES: Ordena a MariaDB que recargue la tabla de permisos inmediatamente.

Paso 4: Preparación del Entorno Python

En esta sección se configura el entorno de ejecución del proyecto.

cd $PROJECT_DIR : Navega al directorio raíz del proyecto (/vagrant).

if [ ! -d ".venv" ]; then python3 -m venv .venv; fi : Crea el entorno virtual (.venv) si aún no existe (idempotencia).

source .venv/bin/activate : Activa el entorno virtual. Esto hace que los comandos como pip y flask apunten a los binarios dentro del .venv.

pip install -r requirements.txt : Instala todas las dependencias de Python listadas en el archivo requirements.txt.

Paso 5: Migraciones y Creación de Tablas

Se utiliza Flask-Migrate (Alembic) para configurar la base de datos con el esquema de modelos de SQLAlchemy.

export FLASK_APP=app.app:create_app: Establece la variable de entorno necesaria para que el comando flask sepa dónde encontrar la instancia de la aplicación (la función create_app en app/app.py).

if [ ! -d "migrations" ]; then flask db init; fi: Comprueba si el directorio migrations existe. Si no, inicializa el repositorio de migraciones de Alembic.

flask db upgrade: Aplica la migración más reciente a la base de datos, lo que resulta en la creación de todas las tablas definidas por los modelos de la aplicación.

Paso 6: Lanzamiento de la Aplicación

Finalmente, se lanza la aplicación Flask y se sale del entorno de aprovisionamiento.

pkill -f 'flask run --host=0.0.0.0': Intenta detener cualquier proceso Flask anterior que pudiera estar ejecutándose, asegurando que solo haya una instancia activa.

nohup flask run --host=0.0.0.0 > /tmp/flask_app.log 2>&1 &: Lanza el servidor Flask:

  • --host=0.0.0.0: Permite que la aplicación sea accesible desde fuera de la VM (a través del puerto reenviado en Vagrantfile).
  • nohup ... &: Ejecuta el comando en segundo plano (&) y lo hace inmune a colgarse (nohup), manteniendo el servidor activo incluso después de que termine la sesión de aprovisionamiento. La salida se redirige al archivo /tmp/flask_app.log.


deactivate: Sale del entorno virtual de Python.

Iniciar el Entorno Virtual con Vagrant

Desde la terminal, navega al directorio `flask_testing_project/` y ejecuta el siguiente comando:

 vagrant up

Cuando el proceso finalice, podrás acceder a la aplicación en `http://localhost:5000`.

¿Has podido ver qué ocurría en cada paso? ¡Prueba a entrar en la máquina virtual y ver cómo está funcionando todo por dentro!


Parte 2: Aprovisionamiento mediante un playbook de Ansible

El aprovisionamiento con Ansible ofrece varias ventajas frente al uso de scripts de shell. Mientras que los scripts de shell son imperativos, es decir, detallan los pasos exactos para lograr un objetivo, Ansible adopta un enfoque declarativo, donde especificamos el estado deseado del sistema y Ansible se encarga de llevarlo a cabo. Esto permite una mayor idempotencia, es decir, que la configuración se pueda aplicar varias veces sin causar efectos no deseados. Además, Ansible facilita la gestión de infraestructuras grandes de forma escalable y reproducible, lo que hace que sea más fácil mantener entornos consistentes y automatizados en comparación con los scripts tradicionales. Un aspecto clave de Ansible es que permite aprovisionar diferentes máquinas sin importar el sistema operativo o la infraestructura subyacente, ya que simplemente le indicamos qué necesitamos y Ansible se encarga de implementar la configuración adecuada, independientemente de cómo o dónde se ejecute.


Paso 1: Modificar el Vagrantfile

Modifica el Vagrantfile para ejecutar un playbook de Ansible en lugar del script provision.sh:

# Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  
  # 1. Sistema Operativo
  config.vm.box = "ubuntu/jammy64" 
  
  # 2. Mapear puerto de Flask (usando 5001 como puerto de host para evitar colisiones)
  # Aplicación accesible en http://localhost:5001
  config.vm.network "forwarded_port", guest: 5000, host: 5001
  
  # 3. Aprovisionamiento: Ejecuta el Playbook de Ansible
  config.vm.provision "ansible" do |ansible|
    # Especifica que Vagrant debe ejecutar el archivo playbook.yml
    ansible.playbook = "playbook.yml"
    
    # Ejecutar tareas con privilegios de root (sudo)
    ansible.become = true
    
    # Asegura que se ejecuta en el host de la MV
    ansible.limit = "all" 
  end
end

Paso 2: Crea el playbook.yml

En la raíz del proyecto, al mismo nivel que el Vagrantfile, crea el playbook.yml.

---
- name: Configurar Entorno Flask y MariaDB
  hosts: all
  become: yes # Ejecutar tareas con privilegios de root (sudo)
  vars:
    project_dir: /vagrant
    env_file: "{{ project_dir }}/.env"

  tasks:
    # --- 1. Saneamiento y Carga de Variables de Entorno (.env) ---
    - name: Cargar variables de entorno como JSON y establecer hechos
      ansible.builtin.shell: |
        # 1. Leer el archivo .env, limpiar CRLF (\r) y líneas vacías/comentarios.
        # 2. Convertir el formato KEY=VALUE al formato JSON "KEY":"VALUE".
        CLEAN_ENV=$(
          cat {{ env_file }} | 
          tr -d '\r' | 
          grep -vE '^\s*#' |
          grep -vE '^\s*$' | 
          awk -F'=' '{ gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print "\""$1"\":\""$2"\"" }' |
          paste -s -d ',' -
        )
        
        # 3. Imprimir el JSON completo. Ejemplo: {"KEY1":"VALUE1","KEY1":"VALUE2"}
        echo "{ $CLEAN_ENV }"
      register: env_output
      changed_when: false
      tags: [config, env]

    - name: Establecer variables de entorno limpias como hechos de Ansible
      ansible.builtin.set_fact:
        env_vars: "{{ env_output.stdout | from_json }}"
      tags: [config, env]
      
    # --- 2. Preparación del Sistema ---
    - name: Forzar la actualización del índice de paquetes (apt update)
      ansible.builtin.apt:
        update_cache: yes
      tags: [install, base]

    - name: Instalar MariaDB, Python y utilidades necesarias
      ansible.builtin.package:
        name: 
          - mariadb-server
          - python3-pip 
          - git
          - python3-dev
          - python3-venv
        state: present
      tags: [install, base]

    - name: Instalar PyMySQL a nivel de sistema (Necesario para los módulos 'mysql_*' de Ansible)
      ansible.builtin.pip:
        name: pymysql
        executable: pip3
      tags: [install, db]

    # --- 3. Configuración de MariaDB (Módulos declarativos de MySQL) ---
    - name: Asegurar que el servicio MariaDB esté iniciado y habilitado
      ansible.builtin.service:
        name: mariadb
        state: started
        enabled: yes
      tags: [db]

    - name: Crear Base de Datos y Usuario
      block:
        - name: Crear Base de Datos
          community.mysql.mysql_db:
            name: "{{ env_vars.DATABASE_DB }}" 
            state: present
            encoding: utf8mb4
            collation: utf8mb4_unicode_ci
            login_unix_socket: /run/mysqld/mysqld.sock 
          tags: [db]

        - name: Crear Usuario de Aplicación y Otorgar Permisos
          community.mysql.mysql_user:
            name: "{{ env_vars.DATABASE_USER }}"
            password: "{{ env_vars.DATABASE_PASSWORD }}"
            host: "localhost"
            priv: "{{ env_vars.DATABASE_DB }}.*:ALL" 
            state: present
            login_unix_socket: /run/mysqld/mysqld.sock 
          tags: [db]

    # --- 4. Configuración de la Aplicación Flask ---
    - name: Instalar dependencias de Python a nivel de sistema
      ansible.builtin.pip:
        requirements: "{{ project_dir }}/requirements.txt"
        executable: pip3 # Usamos pip3 del sistema
        state: present
      tags: [app, install]

    - name: Aplicar migraciones de base de datos (Flask-Migrate)
      ansible.builtin.command: python3 -m flask db upgrade # Usamos python3 -m flask
      environment:
        DATABASE_USER: "{{ env_vars.DATABASE_USER }}"
        DATABASE_PASSWORD: "{{ env_vars.DATABASE_PASSWORD }}"
        DATABASE_DB: "{{ env_vars.DATABASE_DB }}"
        DATABASE_HOST: "{{ env_vars.DATABASE_HOST | default('localhost') }}"
        FLASK_APP: app.app:create_app
      args:
        chdir: "{{ project_dir }}"
      become: yes
      become_user: vagrant
      tags: [app, db]

    # --- 5. Lanzar la Aplicación ---
    
    # Tarea 5.1: Detener procesos previos
    - name: Detener instancias de Flask previamente en ejecución
      ansible.builtin.shell: "pkill -f 'python3 -m flask run --host=0.0.0.0'"
      become: yes
      become_user: vagrant
      failed_when: false # CRÍTICO: Ignorar el error si pkill no encuentra nada.
      tags: [app, run]

    # Tarea 5.2: Iniciar Flask en segundo plano con nohup
    - name: Iniciar Flask en segundo plano con nohup
      ansible.builtin.shell: |
        nohup python3 -m flask run --host=0.0.0.0 > /tmp/flask_app.log 2>&1 &
      environment:
        FLASK_APP: app.app:create_app
        DATABASE_USER: "{{ env_vars.DATABASE_USER }}"
        DATABASE_PASSWORD: "{{ env_vars.DATABASE_PASSWORD }}"
        DATABASE_DB: "{{ env_vars.DATABASE_DB }}"
        DATABASE_HOST: "{{ env_vars.DATABASE_HOST | default('localhost') }}"
      args:
        chdir: "{{ project_dir }}"
      become: yes
      become_user: vagrant
      tags: [app, run]

    - name: Verificar que Flask se esté ejecutando
      ansible.builtin.shell: "ps aux | grep -v grep | grep 'python3 -m flask run --host=0.0.0.0'"
      register: flask_status
      failed_when: flask_status.rc != 0 and 'python3 -m flask run' not in flask_status.stdout
      tags: [app, run]

    - name: Mostrar mensaje de acceso
      ansible.builtin.debug:
        msg: "✅ ¡Aprovisionamiento COMPLETO! La aplicación Flask está en segundo plano. Acceda en http://localhost:5001"
      tags: [app, run]


Paso 2: Explicación detallada del playbook.yml

Carga y Saneamiento de Variables

Este bloque es fundamental para la estabilidad. Resuelve los problemas de caracteres invisibles del archivo .env que suelen causar fallos en MariaDB, cargando las variables en un formato JSON limpio.

Ansible lee el archivo /vagrant/.env, utiliza herramientas de shell como tr y awk para eliminar cualquier carácter de retorno de carro (CRLF) o espacio sobrante, y lo formatea como un objeto JSON.

El módulo set_fact carga este JSON limpio en la variable env_vars, garantizando que nombres como la base de datos y el usuario sean cadenas de texto perfectas.

Instalación de Componentes Base

Esta sección configura el sistema operativo para ejecutar la base de datos y la aplicación Python.

sudo apt update: Se fuerza la actualización del índice de paquetes para asegurar que se instalen las versiones más recientes.

sudo apt install mariadb-server python3-pip python3-dev ...: Instala todos los paquetes necesarios del sistema: el servidor de MariaDB, las herramientas de Python 3, y las cabeceras de desarrollo de Python (python3-dev), que son necesarias para compilar bibliotecas binarias como mysqlclient.

pip3 install pymysql: Instala la biblioteca PyMySQL a nivel de sistema. Esta biblioteca no es para la aplicación Flask, sino para que Ansible pueda comunicarse con MariaDB y crear la base de datos y el usuario en las siguientes tareas.

Configuración de MariaDB

Este bloque configura la base de datos y el usuario de manera idempotente utilizando los módulos declarativos de Ansible, que reemplazan la necesidad de ejecutar comandos SQL manuales.

La tarea Asegurar que el servicio MariaDB esté iniciado y habilitado equivale a: sudo systemctl enable mariadb / sudo systemctl start mariadb: Asegura que el servicio MariaDB esté habilitado para arrancar con el sistema y lo inicia inmediatamente.

La tarea Crear Base de Datos utiliza el valor limpio de DATABASE_DB para crearla con la codificación correcta (utf8mb4).

La tarea Crear Usuario de Aplicación y Otorgar Permisos utiliza los valores limpios de DATABASE_USER y DATABASE_PASSWORD para crear el usuario y otorgarle todos los permisos (GRANT ALL PRIVILEGES) sobre la base de datos recién creada.

Instalación de Dependencias Python

La tarea Instalar dependencias de Python a nivel de sistema equivale a: sudo pip3 install -r /vagrant/requirements.txt: Instala todas las dependencias listadas en el archivo requirements.txt a nivel global del sistema.

Migraciones y Creación de Tablas

Se utiliza Flask-Migrate para configurar la base de datos con el esquema de modelos de SQLAlchemy.

La tarea Aplicar migraciones de base de datos (Flask-Migrate) ejecuta el comando: export FLASK_APP=app.app:create_app

python3 -m flask db upgrade: Aplica la migración más reciente a la base de datos, creando todas las tablas definidas por los modelos de la aplicación (o actualizándolas si ya existen). Esta tarea se ejecuta como el usuario vagrant para asegurar los permisos de acceso a los archivos del proyecto.

Lanzamiento de la Aplicación

Finalmente, se lanza la aplicación Flask y se asegura su continuidad.

La tarea Detener instancias de Flask previamente en ejecución ejecuta: pkill -f 'python3 -m flask run --host=0.0.0.0': Intenta detener cualquier proceso Flask anterior. Es crucial que la tarea tenga la opción failed_when: false, para que Ansible no falle si no se encuentra ningún proceso que matar.

La tarea Iniciar Flask en segundo plano con nohup ejecuta: nohup python3 -m flask run --host=0.0.0.0 > /tmp/flask_app.log 2>&1 &: Lanza el servidor Flask:

--host=0.0.0.0: Permite que la aplicación sea accesible desde fuera de la VM (a través del puerto reenviado en Vagrantfile).

nohup ... &: Ejecuta el comando en segundo plano (&) y lo hace inmune a colgarse (nohup), manteniendo el servidor activo. La salida se redirige al archivo /tmp/flask_app.log.

Verificación y Mensaje Final: Las últimas tareas verifican que el proceso Flask esté corriendo y muestran un mensaje de éxito, indicando que la aplicación está lista en http://localhost:5001.

La aplicación Flask estará disponible en `http://localhost:5001` cuando finalice el aprovisionamiento.

¿Qué has podido observar ahora en la terminal en la etapa de aprovisionamiento? ¿Crees que la MV estará funcionando igual que en el aprovisionamiento por shell script?

Conclusión

Este tutorial te ha guiado a través de dos métodos de aprovisionamiento de una aplicación Flask en Vagrant, utilizando un script de shell y un playbook de Ansible. ¡Ahora deberías tener una mejor comprensión del aprovisionamiento automático y de cómo desplegar aplicaciones en entornos virtuales reproducibles!

¡Enhorabuena si has llegado hasta aquí! Y, recuerda, las máquinas virtuales consumen recursos de tu equipo, apágalas y elimínalas cuando ya no las necesites.