Author: Walter Leturia

  • Terraform + AWS – Hands on Lab

    Terraform + AWS – Hands on Lab

    Hemos hablado antes de Terraform y sus ventajas para poder desplegar diferentes recursos. Así como configuración de variables y de entornos de despliegue.

    Ahora solo debemos tener un diagrama para guiarnos y que mejor lugar que el propio aws para poder encontrarlo.

    Elegí la arquitectura de ejemplo de Simple File Manager for Amazon EFS

    Hoy nos enfocaremos en los siguientes componentes:

    1. Amazon S3 web source bucket
    2. Amazon CloudFront
    3. Amazon Cognito user pool

    ¿Dónde comenzamos?

    El primer paso es instalar e inicializar el proveedor de AWS para Terraform en nuestro main.tf

    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
      }
    }

    Y su configuración, en mi caso utilizaré us-east-2

    provider "aws" {
      region = "us-east-2"
    }

    Para poder desplegar esta arquitectura es necesario tener el CLI de AWS en el equipo que ejecutará el plan de terraform, además de la configuración de permisos necesaria para que esté vinculado a nuestra cuenta.

    S3 Bucket

    Debido a que CloudFront recibe parámetros de S3 este será el primer recurso que crearemos.

    Para mantener organizado el proyecto crearé un archivo por cada recurso y ahí colocaré todas las configuraciones adicionales, en este caso s3.tf

    resource "aws_s3_bucket" "web_bucket" {
      bucket = "web-source-bucket"
    
      tags = {
        Name = "WebSourceBucket"
      }
    }

    Debemos permitir el acceso público a este bucket, el recurso se llama aws_s3_bucket_public_access_block y necesita el id del bucket que hemos declarado, podemos utilizar propiedades de nuestros recursos accediendo a RECURSO.NOMBRE_RECURSO.PROPIEDAD

    resource "aws_s3_bucket_public_access_block" "block" {
      bucket = aws_s3_bucket.web_bucket.id
    
      block_public_acls   = false
      block_public_policy = false
      ignore_public_acls  = false
      restrict_public_buckets = false
    }

    Y como última configuración la política de lectura pública

    resource "aws_s3_bucket_policy" "public_read" {
      bucket = aws_s3_bucket.web_bucket.id
    
      policy = jsonencode({
        Version = "2012-10-17",
        Statement = [
          {
            Sid       = "PublicReadGetObject",
            Effect    = "Allow",
            Principal = "*",
            Action    = ["s3:GetObject"],
            Resource  = "${aws_s3_bucket.web_bucket.arn}/*"
          }
        ]
      })
    }

    CloudFront

    La configuración de CloudFront es un poco más extensa de lo que hemos visto hasta ahora, utilicemos el archivo cloudfront.tf

    resource "aws_cloudfront_distribution" "cdn" {
      origin {
        domain_name = aws_s3_bucket.web_bucket.bucket_regional_domain_name
        origin_id   = "s3Origin"
    
        s3_origin_config {
          origin_access_identity = ""
        }
      }
    
      enabled             = true
      is_ipv6_enabled     = true
      default_root_object = "index.html"
    
      default_cache_behavior {
        allowed_methods  = ["GET", "HEAD"]
        cached_methods   = ["GET", "HEAD"]
        target_origin_id = "s3Origin"
    
        viewer_protocol_policy = "redirect-to-https"
    
        forwarded_values {
          query_string = false
          cookies {
            forward = "none"
          }
        }
      }
    
      restrictions {
        geo_restriction {
          restriction_type = "none"
        }
      }
    
      viewer_certificate {
        cloudfront_default_certificate = true
      }
    
      comment = "LeoCorp"
      tags = {
        Name = "CloudFrontWebCDN"
      }
    }

    Cognito User Pool

    resource "aws_cognito_user_pool" "user_pool" {
      name = "leocorp"
    
      password_policy {
        minimum_length    = 8
        require_uppercase = true
        require_lowercase = true
        require_numbers   = true
        require_symbols   = false
      }
    
      auto_verified_attributes = ["email"]
      alias_attributes         = ["email"]
    }

    ¿Qué sigue?

    Es recomendable tener un archivo outputs.tf el cual muestre información importante para poder identificar los recursos que han sido creados, es común que estos valores se utilicen en posteriores pasos en un pipeline y podemos acceder a ellos a través del entorno.

    outputs.tf

    output "bucket_name" {
      value = aws_s3_bucket.web_bucket.bucket
    }
    
    output "cloudfront_domain" {
      value = aws_cloudfront_distribution.cdn.domain_name
    }
    
    output "cognito_user_pool_id" {
      value = aws_cognito_user_pool.user_pool.id
    }

    Pasos próximos

    No hemos terminado, de momento nuestro plan puede generar recursos pero debemos considerar el uso de variables.

    Ten en cuenta que nuestro S3 necesita un index.html para poder mostrar esta información. Esto también podemos automatizarlo en un pipeline.

    Realizaremos estas mejoras en la segunda entrega de esta serie 😀

  • Deep Learning – Reducción de dimensionalidad (PCA)

    Deep Learning – Reducción de dimensionalidad (PCA)

    Exploraremos esta técnica y su uso en cuanto reducción de dimensionalidad. Buscaremos comprender su funcionamiento y utilidad en proyectos de inteligencia artificial

    Usaremos MNIST para desarrollar este laboratorio. Descarga el dataset y realiza la normalización y aplanamiento de dimensiones

    Entendamos PCA

    PCA es una técnica lineal que permite la proyección de datos en un espacio de menor dimensión preservando la mayor cantidad posible de varianza original.

    Para utilizarlo en nuestro proyecto debemos importarlo desde sklearn:

    from sklearn.decomposition import PCA

    Una vez implementado PCA definiremos la cantidad de componentes que deseamos trabajar. ¿Que indica la cantidad de componentes? Indica el número de componentes principales que se desean conservar después de realizar la reducción de dimensionalidad:

    pca = PCA(n_components=2)
    pca_train = pca.fit_transform(x_train)
    pca_test = pca.transform(x_test)

    ¿Esto afecta la estructura de nuestro conjunto de datos?

    Revisemos las variables pca_train y x_train

    x_train tiene la siguiente estructura:

    Mientras que pca_train tiene solo:

    Reconstrucción

    El método inverse_transform nos dará las imágenes reconstruidas a partir del aprendizaje de la transformación realizada con fit_transform:

    x_train_reconstructed_pca = pca.inverse_transform(pca_train)
    x_test_reconstructed_pca = pca.inverse_transform(pca_test)

    Es decir un arreglo de 784

    Métricas de error

    ¿Podemos tener números que muestren el error? Aplica MSE y podrás tener una métrica que sustente la interpretación de tus resultados

    mse_train_pca = mean_squared_error(x_train, x_train_reconstructed_pca)
    mse_test_pca = mean_squared_error(x_test, x_test_reconstructed_pca)

    print(f"Error de reconstrucción en el entrenamiento con PCA: {mse_train_pca:.4f}")
    print(f"Error de reconstrucción en la validación con PCA: {mse_test_pca:.4f}")

    Nos da un número bastante bajo

    ¿Quiere decir que vamos por buen camino? Validemos mostrando las reconstrucciones de nuestro conjunto de evaluación.

    Decodifiquemos

    Para facilitar la impresión de los resultados crearemos un método que nos permita reutilizarlo según el conjunto que utilicemos

    def mostrar_imagenes(dataset, show=10):
      plt.figure(figsize=(20, 4))
      for i in range(show):
        ax = plt.subplot(2, show, i + 1)
        plt.imshow(dataset[i].reshape(28, 28))
        plt.gray()
        ax.get_xaxis().set_visible(False)
        ax.get_yaxis().set_visible(False)

    Y podemos visualizar los datos de nuestro conjunto de datos antes y después de utilizar la técnica PCA

    ¿Qué pasó? ¿Por qué mi métrica me mostraba un número bajo y los números no se entienden? Recuerda como funciona MSE. Penaliza los errores grandes y tus datos están normalizados entre 0 y 1. La métrica que debemos utilizar es RMSE:

    rmse_train_pca = root_mean_squared_error(x_train, x_train_reconstructed_pca)
    rmse_test_pca = root_mean_squared_error(x_test, x_test_reconstructed_pca)
    
    print(f"Error de reconstrucción (RMSE) en el entrenamiento con PCA: {rmse_train_pca:.4f}")
    print(f"Error de reconstrucción (RMSE) en la validación con PCA: {rmse_test_pca:.4f}")

    Ten en cuenta que esta métrica suavizará el error porque será un promedio, si quieres penalizar de una manera más severa puedes obtener una métrica de RMSE por cada imagen y calcular el promedio.

    ¿Cómo optimizamos el modelo?

    Prueba subiendo el número de componentes hasta que obtengas un buen resultado, ¿Qué te parece si probamos ahora con 50?

    Nuestras métricas disminuyeron notablemente. ¿Y como le fue a la reconstrucción de imágenes?

    Ahora son más legibles

    El número perfecto

    ¿Cómo podríamos determinar el número adecuado de componentes? Si no utilizas un valor de componentes PCA utilizará la mayor cantidad disponible con tu conjunto de datos. Podemos hacer lo siguiente:

    pca_full = PCA().fit(x_train)
    plt.plot(np.cumsum(pca_full.explained_variance_ratio_))
    plt.xlabel('Número de componentes')
    plt.ylabel('Varianza acumulada')
    plt.grid(True)
    plt.show()

    Esto nos dará la relación entre la cantidad de componentes y la varianza

    Podemos interpretar que un número de componentes cercano al 100 nos dará un buen resultado y que a partir de 200 componentes la ganancia no es significativa…

    Conclusiones

    • Las métricas de error deben aplicarse según la finalidad de los modelos, en este caso al aplicar MSE con data normalizada entre 0 y 1 podemos tener una falsa orientación de que el modelo está funcionando correctamente
    • Para elegir el número ideal de componentes no es necesario ir con un número máximo, podemos considerar el uso de números óptimos. Esto es beneficioso cuando el conjunto de datos es extenso.
    • Considera el uso de paradas anticipadas, así no exploras todas las combinaciones si no es necesario

    Te recomiendo considerar el uso de métricas de error como: SSIM o PSNR. Para comparación de reconstrucciones visuales. 😉

  • Terraform – Variables y Entornos

    Terraform – Variables y Entornos

    En la anterior entrega hablamos sobre terraform, sus ventajas y sintaxis básica. Pero dejamos un tema sin cubrir, si bien hablamos de despliegue de entornos de manera rápida. ¿Qué pasa cuando queremos tener múltiples configuraciones?

    En esta publicación hablaremos de las variables y como podemos mantener nuestro proyecto.

    Variables

    Variables, variables, variables…. Es la base de la configuración. En HCL también tenemos variables. ¿Cómo las declaramos?

    Creamos un archivo variables.tf

    variable "nginx_external_port" {}

    Listo, con eso ya podemos utilizar variables en nuestro main.tf de la siguiente forma:

    resource "docker_container" "container_nginx" {
      ...
      ports {
        ...
        external = var.nginx_external_port
        ...
      }
    }

    Las variables se declaran teniendo en cuenta un nombre, usamos snake_case. Ahora cuando ejecutemos el plan de terraform tendremos lo siguiente:

    Nos pide un valor, excelente! ¿Podemos mejorarlo? Sí, recuerda que el código debe ser legible para otros programadores, no solo para el que lo desarrolló.

    Descripciones

    Podemos añadirle una descripción que permita entender el valor que espera nuestro proyecto:

    variable "nginx_external_port" {
      description = "Puerto externo del nginx"
    }

    Ahora al verificar el plan de ejecución tendremos un mensaje como el siguiente:

    Esto no te quita la responsabilidad de seguir buenas prácticas, tus variables no pueden ser variable0 asdasd!!!

    Restricciones

    Podemos también restringir el tipo de dato de nuestra variable, así evitamos que se pueda cometer el siguiente error:

    Para esto hacemos uso de la propiedad type

    variable "nginx_external_port" {
      description = "Puerto externo del nginx"
      type        = number
    }

    Listo, ahora solo podremos colocar valores numéricos

    Ahora será un error de validación que está controlado debido a que estamos esperando un número.

    Default

    Simplifiquemos el ciclo de desarrollo a nuestros amigos dev, si el proyecto puede tener valores por defecto escríbelos.

    variable "nginx_external_port" {
      description = "Puerto externo del nginx"
      type        = number
      default     = 3000
    }

    Nuestro plan de ejecución podrá ejecutarse sin tener que ingresar estas variables

    ¿Y que tal si quiero definir otros valores?

    Ya has definido tus variables, sabes que valor debe recibir e inclusive valores por defecto que tendrá. Pero ¿Y si no quiero configurar mi despliegue con los valores por defecto? ¿De que manera puedo pasar valores o configurarlos?

    Regresemos el archivo variables.tf a este estado

    variable "nginx_external_port" {
      description = "Puerto externo del nginx"
      type        = number
    }

    Y ahora crearemos un archivo llamado terraform.tfvars donde colocaremos lo siguiente:

    nginx_external_port = 3000

    Si ejecutamos nuestro plan de terraform tendremos la misma salida!

    Recuerda que el archivo terraform.tfvars sobreescribe los valores de variables.tf.

    Entornos

    Para este proyecto hemos ejecutado previamente este comando

    terraform init

    El cual se encargó de dos cosas: Descargó las dependencias de proveedor e inicializó el proyecto en el entorno/mesa de trabajo default

    ¿Cómo podemos verificarlo? Ejecuta el siguiente comando

    terraform workspace show

    Te mostrará una salida igual a esta:

    Tenemos otra forma de ver los entornos con el comando:

    terraform workspace list

    Te mostrará la lista de entornos que tienes en el proyecto y pondrá un asterisco de prefijo en el entorno en el que te encuentres trabajando.

    Uso de entornos

    Para crear un nuevo entorno puedes utilizar el siguiente comando:

    terraform workspace new NOMBRE_DEL_ENTORNO

    Configuraciones por entorno

    Dentro de terraform podemos utilizar el nombre del entorno a través de la siguiente propiedad:

    terraform.workspace

    Modifiquemos nuestro archivo terraform.tfvars. En vez de un dato numérico utilicemos un diccionario:

    nginx_external_port = {
      default = 3000
      prod    = 8080
    }

    Para utilizar este valor en nuestro main.tf debemos realizar el siguiente cambio:

    resource "docker_container" "container_nginx" {
      ...
      ports {
        ...
        external = var.nginx_external_port[terraform.workspace]
      }
    }
    

    Y como recomendación actualiza el tipo de dato en terraform.tfvars y no solo elimines esta validación. Una buena práctica es declarar el objeto que esperamos:

    variable "nginx_external_port" {
      description = "Puerto externo del nginx"
      type = object({
        default = number
        prod    = number
      })
    }

    De esta manera si olvidamos colocar un valor tendremos una validación y no un error durante ejecución

    Posibles errores

    Si al ejecutar el plan de terraform tienes el siguiente error:

    Es porque la variable ya no contiene un valor numérico, ahora recibe un valor de tipo diccionario. Revisa el tipo que estás declarando tu variables.tf

    Si te da un error de tipo:

    Es porque el entorno que tienes declarado en tu ordenador no cuenta con un valor en tu diccionario.

    Conclusiones

    ¿Para qué sirven los entornos? Hablamos de infraestructura como código. Las cadenas de conexión, puertos o inclusive configuraciones de aplicaciones varían según el entorno, sea desarrollo, qa, producción, etc…

    Haciendo uso de terraform y configuración de variables podemos facilitar el despliegue según nuestras necesidades. Además, podemos hacer seguimiento de estas configuraciones en el archivo terraform.tfvars.

    Practica creando diferentes entornos y desplegando tu infraestructura con valores personalizados para cada uno. Recuerda que cada entorno tiene su propio historial de cambios!

  • Docker Compose – Configuremos Jenkins!

    Docker Compose – Configuremos Jenkins!

    Si estás leyendo esto seguro que ya configuraste una imagen en docker y estás corriendo un contenedor (tranquilo, si no lo haces aún puedes revisar esto).

    Y quizá este escenario se te hace conocido. Tienes que ejecutar un contenedor en diferentes puertos, diferentes stages, etc etc…

    Y de seguro que tienes un bloc de notas con algo parecido a esto:

    docker volume create dev-data
    
    docker run -d \
      --name app-dev \
      -p 8081:80 \
      -e ENV=development \
      -v dev-data:/data \
      leocorp/test

    Es un dolor de cabeza! Lo bueno es que tiene solución, con docker-compose pasamos de tener todas nuestras configuraciones en un bloc de notas a un archivo .yaml.

    Paso a Paso

    Para esta guía vamos a desplegar Jenkins, esta herramienta de CD/CI nos permite poder ejecutar pipelines!

    Primera línea de nuestro archivo docker-compose.yaml:

    version: '3.9'

    El file format 3 está orientado a su uso en Docker Swarm y entornos modernos de docker, pero eso no lo cubriremos acá aún. Recuerda que a partir de docker compose v1.27.0 esta línea no es obligatoria.

    Servicios

    En nuestro archivo, siguiendo la indentación de version definiremos nuestros servicios:

    services:
      jenkins:
        image: jenkins/jenkins:lts
        ports:
          - "8080:8080"
          - "50000:50000"
        restart: unless-stopped

    En este bloque estamos definiendo el uso de Jenkins, específicamente la versión LTS (no es recomendable esto en aplicaciones de producción, deberías usar una versión fija). Además, especificamos los puertos, esta imagen trabaja con los puertos 8080 y 50000.

    Y estamos dándole la característica a este contenedor de que se reinicie y busque estar siempre activo al menos que lo hayamos detenido manualmente.

    Volúmenes

    Recuerda que Jenkins trabaja con archivos para su configuración, todo esto se guarda dentro de /var/jenkins_home en nuestro contenedor.

    Si queremos mantener nuestras configuraciones persistentes (y no perder todo en cada reinicio) debemos guardar esto fuera del contenedor.

    ¿Cómo lo hacemos? En nuestro archivo, siguiendo la indentación de services:

    volumes:
      jenkins_home:

    En nuestra configuración definimos el acceso a través de jenkins_home, esto crea, en caso no exista, el volumen y lo habilita para nuestros servicios.

    ¿Cómo lo uso en mis servicios?

    Debemos modificar nuestro servicio de la siguiente manera:

    services:
      jenkins:
        ...
        volumes:
          - jenkins_home:/var/jenkins_home
        ...

    La relación es la siguiente:

    • jenkins_home: el nombre del volumen Docker que tenemos declarado líneas a bajo
    • /var/jenkins_home: la carpeta dentro del contenedor donde Jenkins guarda su configuración, jobs, plugins, etc.

    Levantemos nuestros servicios!

    Es muy sencillo, en la carpeta donde tienes el archivo docker-compose.yaml abre una terminal y ejecuta el siguiente comando:

    docker compose up -d

    Deberías ver lo siguiente:

    Nos muestra el nombre del volumen, red y contenedor creado. Esto está con el prefijo de la carpeta donde tenemos alojado el archivo docker-compose.yaml.

    Una vez iniciados los servicios puedes acceder a tu portal de Jenkins desde localhost:8080

    En el primer uso de Jenkins debemos colocar una contraseña, esta contraseña la encontrarás ejecutando el siguiente comando:

     docker logs dc-jenkins-jenkins-1

    o

    docker compose logs -f

    Y busca la siguiente línea en la consola, esa es tu contraseña!

    Escribiré una entrada detallando las configuraciones propias de Jenkins, te recomiendo sumergirte en esta nueva plataforma haciendo uso de la documentación oficial

    Posibles errores

    Si te da un error de conexión con el daemon, probablemente tu servicio de docker no se encuentra en ejecución

    Puedes comprobar tus servicios de docker con

    docker ps

    ¿Cómo debería verse?

    Conclusiones

    ¿Por qué deberíamos usar hojas de configuración y no guardar los comandos en un bloc de notas?

    Docker Compose nos brinda lo siguiente:

    • Agrupación de servicios, todos los contenedores en un solo archivo.
    • Ejecución simple, up y detached.
    • Mismo entorno, tenemos toda nuestra configuración en un archivo YAML fácil de versionar.

    Ahora solo te queda empaquetar todo! Puedes practicar migrando a docker compose tus bases de datos, aplicaciones, volúmenes, etc!. Suerte 😉

  • Terraform – Primeros pasos

    Terraform – Primeros pasos

    ¿Qué es terraform? ¿Por qué los DevOps lo aman? Imagina levantar servidores, bases de datos, redes, contenedores… todo con solo algunas líneas de código. ¿Suena interesante verdad?

    Todo esto lo hace posible Terraform, Infraestructura como Código (IAC). Desarrollado por HashiCorp, permite definir nuestra infraestructura en archivos de configuración!

    Ventajas de usar Terraform

    • Poner en línea servidores
    • Crear redes virtuales y balanceadores de carga
    • Automatizar y versionar entornos
    • Reproducir entornos completos (desarrollo/staging/producción)

    ¿Qué lenguaje usa Terraform?

    Usa su propio lenguaje HCL (HashiCorp Configuration Language)

    resource "leocorp_nginx" "nginx" {
      name  = "nginx_leocorp"
      image = "nginx:latest"
    }

    No es un lenguaje de programación tradicional, es más un lenguaje declarativo (tú describes el estado, terraform se encarga)

    Manos a la obra

    Levantaremos un servidor nginx usando Terraform, todo desde 0 y paso a paso.

    Para esto ya debes tener instalado en tu equipo Docker y Terraform

    Orden Orden Orden

    Crearemos una carpeta dedicada para este proyecto

    mkdir leocorp-terraform && cd leocorp-terraform

    ¿Qué archivos tendremos en esta carpeta?

    Solemos utilizar los siguientes archivos:

    • main.tf: El principal archivo, es el que tendrá nuestra arquitectura
    • variables.tf: Hoja de configuración, al tener esto podemos trabajar con múltiples entornos
    • outputs.tf: Útil para describir lo que estamos creando

    Arquitectura Principal

    En esta oportunidad nos enfocaremos en main.tf debemos primero elegir nuestro proveedor, el plugin de proveedor nos permite añadir funcionalidades e interactuar con los componentes que necesitamos. En este caso utilizaremos kreuzwerker/docker en su versión más reciente 3.0.2.

    terraform {
      required_providers {
        docker = {
          source  = "kreuzwerker/docker"
          version = "3.0.2"
        }
      }
    }

    Una vez instalado el plugin debemos configurar nuestro proveedor, como realizaremos esta prueba de manera local podemos colocar lo siguiente:

    
    provider "docker" {}

    Finalmente los recursos que queremos configurar, en este caso levantaremos un contenedor de nginx y configuramos el puerto 8080 en nuestro ordenador apuntando al puerto 80 en el contenedor.

    resource "docker_container" "nginx" {
      name  = "nginx"
      image = "nginx:latest"
      ports {
        internal = 80
        external = 8080
      }
    }

    ¿Ya estamos listos? Ya casi, debemos inicializar y aplicar la configuración

    terraform init

    Al inicializar se descargarán los plugins necesarios para poder desplegar nuestra arquitectura

    Una vez inicializado podemos levantar nuestra arquitectura

    terraform apply

    Si todo va bien nos mostrará una lista de recursos que serán modificados/creados/eliminados

    La aprobación es manual, descuida solo por ahora 😉

    Una vez aprobado el plan de ejecución nuestro contenedor ya debería estar desplegado, podemos verificar con los logs

    docker logs nginx

    O accediendo al puerto 8080 desde nuestro navegador, donde veremos lo siguiente

    Consideraciones

    Ten en cuenta que estamos conectándonos a nuestro daemon de docker local, si no está ejecutándose el servicio puedes tener un error como este:

    Si tienes problemas con los puertos prueba cambiando el puerto de tu host. En vez de 8080 podrías probar otros como 3000 o 4200.

    Conclusiones

    Hemos manipulado contenedores en nuestro daemon de docker de manera local y todo con archivos de configuración y la herramienta terraform!

    Aún nos falta optimizar esto y realizar configuraciones que consideren diferentes valores según el entorno que deseamos desplegar, no te preocupes eso vendrá en la siguiente entrada 😀

  • Docker – En simples palabras

    Docker – En simples palabras

    ¿Te imaginas ejecutar tu aplicación en cualquier lugar sin preocuparte por el entorno? Docker lo hace posible. Primero vamos a conocer qué es Docker, por qué es tan popular entre desarrolladores y DevOps, y cómo puedes dockerizar tu primera app con Node.js.

    ¿Qué es docker?

    Docker es una plataforma que permite crear, ejecutar y gestionar aplicaciones dentro de contenedores. Estos contenedores tienen la característica de ser ligeros, portables y tener todo lo necesario para que nuestra app pueda ejecutarse (código, dependencias, config, etc).

    Y sí, el problema de “en mi máquina si funciona” se acaba hoy.

    Conceptos clave

    Imágenes

    Las imágenes en docker son plantillas que nos permiten montar contenedores. Contienen lo que necesita nuestro aplicativo: sistema operativo base, dependencias, archivos, comandos de ejecución…

    Contenedores

    Un contenedor es una instancia de nuestra imagen. Un contenedor ejecuta tu aplicación como si estuviera dentro de un propio minisistema.

    Docker Hub

    Así como GitHub y sus proyectos públicos, lo mismo pero diferente. Imágenes públicas listas para usar, tienes a tu disposición bases de datos, lenguajes de programación, sistemas de CI/CD… Y adivina qué, puedes subir tus propias imágenes y que se encuentren disponibles de manera pública.

    Dockericemos algo

    Haremos este tutorial con la aplicación Hello World que escribimos el otro día.

    En la raíz de nuestra aplicación debemos crear un archivo Dockerfile. Este archivo no tiene extensión

    FROM node:22

    La palabra FROM define que imagen usaremos de base para la construcción de nuestro contenedor. ¿Pero de que va? Recuerda que tu aplicación está hecha en Node.js y node utiliza npm como gestor de paquetes. Si tu aplicación estuviera escrita en java o C# tendríamos que cambiar la imagen base para poder compilar nuestro código.

    Docker descargará automáticamente la imagen (si es que encuentra alguna) desde Docker Hub, puedes también añadir otros repositorios pero eso será más adelante 😉

    Si quieres utilizar otras versiones de node debes darle un vistazo a las imágenes disponibles, en este ejercicio elegí node 22 porque es la que tengo instalado localmente y veo que mi aplicación funciona bien con esta versión.

    WORKDIR /app

    Esta línea de código establece el directorio de trabajo del contenedor.

    Es el equivalente a un cd /app. Las siguientes instrucciones que ejecutes serán dentro de esta carpeta (no te preocupes, docker la crea automáticamente)

    COPY package*.json ./

    Esto copia los archivos package.json y package-lock.json desde tu máquina. Estos tienen la información necesaria de que dependencias tiene tu proyecto.

    RUN npm install

    Esto instala las dependencias dentro de nuestro contenedor.

    COPY . .

    Esto copiará todo el contenido del proyecto (carpetas y subcarpetas también) a la ruta app

    No sobreescribirá node_modules, estos ya docker los está manejando.

    EXPOSE 3000

    Recuerda que nuestra aplicación se ejecutaba en el puerto 3000. Debemos especificarlo dentro de la imagen de docker para que este se encuentre disponible para recibir tráfico.

    CMD ["npm", "start"]

    Tenemos que especificar que comando levantará nuestra aplicación, en este caso al ser una app node.js tenemos comandos que levantan nuestra aplicación desde la parte de package.json.

    Ejecutemos nuestro contenedor

    Para ejecutar nuestro contenedor es necesario primero guardar esta imagen, lo recomendable es utilizar un tag para poder versionarlo con facilidad.

    Ejecuta el siguiente comando en la carpeta donde tienes tu Dockerfile

    docker build -t leocorp/node-server .

    Ya tenemos guardada de manera local nuestra imagen, ya podemos ejecutarla. En este caso estamos ejecutando nuestra imagen haciendo una redirección de puertos, en nuestro ordenador estamos utilizando el puerto 8080 y lo dirigimos al puerto 3000 dentro del contenedor.

    docker run -p 8080:3000 leocorp/node-server

    Has tus propias pruebas y ejecuta tus contenedores en diferentes puertos! no es necesario eliminar las anteriores.

    Conclusiones

    Ya hemos creado nuestra primera imagen docker.

    Docker cache: La forma en la que copiamos los archivos de nuestra aplicación (primero package y luego todo lo demás) permite que docker no reinstale siempre las dependencias cada vez que el código cambia.

    Docker brinda una gran cantidad de funcionalidades, nos falta conversar sobre volúmenes, redes, variables de entorno y demás… Hey pero este ha sido un gran avance, ya has dado tu primer paso en el mundo DevOps!

  • Node.js – Hello World!

    Node.js – Hello World!

    Quizá te estés preguntando ¿Javascript en servidor? ¿No era solo para el frontend? Node.js es un entorno de ejecución que permite utilizar JavaScript en el lado del servidor. Eso es puedes escribir frontend y backend con el mismo lenguaje!

    Un servidor

    Un servidor es una aplicación que escucha y responde a las solicitudes de los clientes, facilitando la comunicación en una red

    Para levantar un servidor en Node.js, podemos utilizar el módulo incorporado http.

    const http = require('http');

    ¿Con eso es suficiente?

    Aún no, pero vamos por buen camino. Ten en cuenta que tu servidor debe tener un punto de acceso, tanto una dirección como un puerto.

    const hostname = '127.0.0.1';
    const port = 3000;

    Debemos tener también un recurso disponible, hagamos algo sencillo. Escribiremos un hello world!

    const server = http
    .createServer((request, response) => {
      response.statusCode = 200;
      response.setHeader('Content-Type', 'text/plain');
      response.end('Hola mundo! Un saludo');
    });

    Revisemos linea por linea:

    • statusCode: Es el código de respuesta que emite el servidor al resolver la solicitud. Puedes ver la lista completa de códigos acá
    • setHeader: La cabecera que hemos añadido Content-Type define el tipo de contenido que está enviando el servidor. En este caso texto
    • end: Finaliza la petición y envía la respuesta al cliente.

    Finalmente, ahora podemos poner en línea nuestro server! (en tu máquina local).

    server.listen(port, hostname, () => {
      console.log(`Bienvenido http://${hostname}:${port}/`);
    });

    Ahora sí tenemos configurado nuestro servidor, está escuchando toda comunicación que ingrese por la dirección 127.0.0.1 y el puerto 3000. Abre tu navegador y compruébalo!

    Te doy una pista, si funciona 😉

    Conclusiones

    En esta entrada hemos publicado un servidor escrito en javascript utilizando Node.js.

    Felicitaciones, has dado tu primer paso en el mundo del backend (si no lo habías hecho antes).

    Próximos pasos

    • Revisa la documentación de Node.js, recuerda que la documentación es tu aliado.
    • Considera el uso de un framework, hará tu vida más sencilla y tienes muchas opciones.
    • Dale un vistazo a TypeScript, es javascript con esteroides
    • Entiende los patrones de diseño, no dejes de lado la teoría
  • Javascript – Variables de Entorno (.env)

    Javascript – Variables de Entorno (.env)

    ​En el desarrollo de aplicaciones, la gestión de variables de entorno permite mantener la seguridad y flexibilidad del software. Separamos configuraciones sensibles, facilitando la adaptación a distintos entornos sin modificar el código. 

    Ejemplos de información que cambia según el entorno:

    • Credenciales
    • Conexiones a base de datos
    • Llaves de integración
    • Entre otros

    ¿Cómo gestionamos variables de entorno en javascript?

    La mayoría de proyectos se gestiona a través de archivos .env, archivos de texto plano que almacena variables en formato clave-valor.

    Ejemplo de un archivo .env

    DB_HOST=localhost
    DB_USER=root
    DB_PASS=root
    DB_PORT=3306

    ¿Cómo leemos este archivo en nuestra aplicación?

    La biblioteca dotenv carga las variables de entorno desde nuestro archivo de entorno y las añade al objeto global process.env

    Para instalar esta librería solo debemos ejecutar en nuestro proyecto el siguiente comando

    npm install dotenv

    ¿Una vez instalado es suficiente? Ya casi, falta una línea más dentro de nuestro proyecto (app.js)

    require('dotenv').config();

    Una vez configurado dotenv, la variable process.env se actualizará y podremos leer sus valores

    const dbHost = process.env.DB_HOST;
    const dbUser = process.env.DB_USER;
    const dbPass = process.env.DB_PASS;

    También tienes la opción de configurar valores por defecto si tu aplicación lo necesita

    const port = process.env.PORT || 3000;

    Buenas prácticas

    • No incluyas archivos .env de producción en tu repositorio. Añade el archivo .env a tu .gitignore para evitar que las credenciales sensibles se publiquen.​
    • Proporciona una plantilla .env. Incluye un archivo .env.example/.env.development que tenga las claves necesarias y valores de ejemplo para que pueda ejecutarse desde un entorno local.
    • No olvides validar! En caso de que una variable sea necesaria para ejecutar la aplicación, puedes validarla con operadores nullish o directamente detener la aplicación.

    ¿Existen alternativas a dotenv?

    A partir de Node.js 22, tenemos habilitado de manera experimental (1.1 – Active development) cargar archivos .env utilizando la opción --env-file.

    node --env-file=.env app.js

    Conclusiones

    El uso de archivos .env en proyectos node/javascript es una práctica recomendada para gestionar configuraciones y credenciales de manera segura y eficiente. 

    La biblioteca dotenv facilita la carga de estos archivos de configuración.

    Aplicando estas configuraciones junto a buenas prácticas de desarrollo de software puedes asegurar una gestión adecuada de credenciales y valores sensibles en tus proyectos. Buena suerte!

  • Introducción a Git: Instalación y Comandos Básicos

    Introducción a Git: Instalación y Comandos Básicos

    En este artículo, veremos cómo instalar Git, configurar credenciales y aprenderemos los comandos fundamentales para comenzar a trabajar con repositorios.

    ¿Cuál es la necesidad?

    Estás trabajando en un documento que debes entregar y de pronto tienes una versión 2, luego versión 3, version final, versión final final final…

    ¿Se te hace familiar este escenario?

    Te cuento algo, los programadores no estamos a salvo de esto tampoco!

    ¿Qué es Git y por qué usarlo?

    Git es un sistema de control de versiones distribuido que permite gestionar el código fuente de proyectos de software. Facilita la colaboración, el seguimiento de cambios y la gestión de versiones de software. ¿Quieres ver funciones más avanzadas?

    Instalación de Git

    Antes de comenzar a usar Git, debemos instalarlo en nuestro sistema.

    Windows

    1. Descarga el instalador desde la página oficial: https://git-scm.com/downloads
    2. Ejecuta el instalador y sigue las instrucciones, asegurándote de seleccionar la opción de usar Git desde la línea de comandos.

    Para verificar la instalación, abre cmd o PowerShell y ejecuta:

    git --version

    Configuración inicial de Git

    Después de instalar Git, es importante configurarlo con nuestro nombre y correo electrónico para que los commits tengan la información correcta.

    git config --global user.name "Walter Leturia"
    git config --global user.email "[email protected]"

    Podemos verificar la configuración con:

    git config --list

    Comandos esenciales de Git

    Inicializar un repositorio

    Para crear un nuevo repositorio en una carpeta:

    git init

    Clonar un repositorio existente

    Si deseas copiar un repositorio remoto a tu máquina local:

    git clone URL_DEL_REPOSITORIO

    Ejemplo con GitHub:

    git clone https://github.com/wleturia/repositorio.git

    Verificar el estado del repositorio

    Para ver los archivos modificados y sin seguimiento:

    git status

    Agregar archivos al área de staging

    git add archivo.txt

    Para agregar todos los archivos modificados:

    git add .

    Crear un commit

    git commit -m "Mensaje del commit"

    Enviar cambios al repositorio remoto

    git push origin main

    (Reemplazar main por la rama en la que estemos trabajando)

    Obtener cambios del repositorio remoto

    git pull origin main

    Crear nueva rama

    El trabajo en ramas es fundamental al momento de utilizar esta tecnología, nos permite ordenar nuestro proyecto. ¿Has escuchado de GitFlow?

    git branch NOMBRE_RAMA
    git checkout NOMBRE_RAMA

    git checkout -b NOMBRE_RAMA

    En este artículo, hemos instalado Git y ejecutado comandos básicos para la gestión de repositorios. Estos conceptos son esenciales para cualquier desarrollador que trabaje en proyectos colaborativos o personales.

    A medida que avances en el uso de Git, descubrirás comandos más avanzados que te ayudarán a gestionar ramas, solucionar conflictos y optimizar tu flujo de trabajo. ¡Explora y sigue practicando!

  • Aprendizaje Profundo – MNIST

    MNIST es un conjunto de datos (dataset) de números escritos a mano y en esta guía entrenaremos un clasificador y lo evaluaremos.

    Usaré Google Colab para esta guía, si no tienes configurado un entorno aún puedes hacerlo siguiendo este post!

    Cargar el conjunto de datos

    Tenemos diversas formas de cargar un conjunto de datos, estos están disponibles en datos tabulados (.csv), imágenes (.jpeg/.png), textos, etc…

    En esta guía lo importaremos haciendo uso de tensorflow. Está disponible a través de la siguiente línea de código

    from tensorflow.keras.datasets import mnist

    Ok lo importé pero falta algo ¿Cómo accedemos a los datos?

    Tenemos un método disponible:

    mnist.load_data()

    Una vez ejecutado podemos entender que es lo que hizo el método, ha descargado mnist.npz y lo devuelve en formato tupla

    Vamos a acceder a estos valores, modificaremos el código a

    (x_train, y_train), (x_test, y_test) = mnist.load_data()

    El dataset está compuesto por 2 tuplas, podemos entender esto como data de entrenamiento y data de validación.

    Veamos el primer valor de x_train y el primer valor de y_train

    ¿Qué valores tenemos ahora?

    • x_train[0]: Un ndarray de tamaño 28×28
    • y_train[0]: un uint8 de valor 5

    Perfecto! están relacionados. Es nuestra data de entrenamiento que podemos entender como imagen y etiqueta.

    Normalicemos nuestros valores

    El proceso de normalización es parte esencial cuando trabajamos con clasificadores. ¿Por qué se normalizan los datos? Las redes neuronales aprenden mejor con datos en rangos pequeños porque evita valores grandes que podrían dificultar la convergencia del modelo.

    x_train, x_test = x_train / 255.0, x_test / 255.0

    ¿Por qué dividimos por 255.0?

    Las imágenes en MNIST tienen píxeles con valores entre 0 y 255 (escala de grises).

    Dividir entre 255.0 normaliza los valores al rango [0, 1]

    Pasamos de esto:

    A esto:

    Aplanemos las capas

    Las imágenes en MNIST tienen forma (28, 28), es decir, una matriz de 28×28 píxeles. Las redes neuronales densas (Dense) esperan vectores como entrada, no matrices. Debemos transformar las imágenes en vectores. 28×28=784.

    Ejecutaremos esta línea de código

    x_train = x_train.reshape(-1, 784)
    x_test = x_test.reshape(-1, 784)

    El dataset se transforma de la siguiente manera:

    ¿Es necesario siempre aplanar los datos? No, esto lo hacemos porque usaremos una red de tipo densa, si quisiéramos usar redes neuronales convolucionales (CNN) en lugar de una red densa, no necesitaríamos aplanar las imágenes. En su lugar, mantendríamos el formato (28,28,1).

    Definamos nuestras capas

    Importaremos el modelo secuencial y las capas densas de Tensorflow

    from tensorflow.keras.models import Sequential
    from tensorflow.keras.layers import Dense

    Obtendremos la estrategia (relacionado al hardware)

    strategy = tf.distribute.get_strategy()

    Y ahora podemos definir nuestra red neuronal, teniendo en cuenta nuestro optimizador, la función de perdida y métricas

    with strategy.scope():
      model = Sequential([
              Dense(512, activation='relu', input_shape=(784,)),
              Dense(256, activation='relu'),
              Dense(128, activation='relu'),
              Dense(10, activation='softmax')
          ])
      model.compile(optimizer='adam',
                      loss='sparse_categorical_crossentropy',
                      metrics=['accuracy'])

    Podemos ver el detalle de nuestra red neuronal con el comando

    model.summary()

    Entrenemos el modelo

    Acá definimos cuantas épocas utilizaremos y de cuanto será el lote de entrenamiento.

    history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_data=(x_test, y_test))

    Evaluemos el modelo

    Ahora evaluaremos el modelo con nuestro conjunto de validación

    test_loss, test_acc = model.evaluate(x_test, y_test)

    Tenemos un resultado de 97% de exactitud, felicitaciones!