DEV Community

Jhorman Villanueva
Jhorman Villanueva

Posted on • Edited on

Orquestando contenedores serverless para IoT con AWS ECS Fargate

En este blog voy a construir una plataforma IoT sobre AWS usando el servicio Amazon ECS como orquestador de contenedores integrando dos tecnologías que me apasionan: computación en la nube e IoT. Un orquestador de contenedores permite gestionar y ejecutar múltiples contenedores donde cada uno cumple una función específica, siguiendo una arquitectura de microservicios. Toda la infraestructura será aprovisionada con Terraform, aplicando las buenas prácticas de Infraestructura como código (IaC). Esta infraestructura soportará el flujo completo de datos: desde un dispositivo IoT que envía lecturas de sus sensores, hasta su procesamiento, almacenamiento y visualización en la nube.

Empecemos hablando entonces un poco de lo que es IoT.

La mayoría de los proyectos en IoT (Internet de Things) se basan en la siguiente arquitectura de referencia de 3 capas propuesta por Miao Wu e investigadores de la universidad de Beijing en el año 2010.

iot-reference-architecture

En la capa de percepción se tiene los dispositivos IoT con la capacidad de medir diferentes variables de un entorno físico como temperatura, humedad, luminosidad, movimiento, pH, entre otras, o actuar en el entorno como encender o apagar un bombillo, activar una alarma o abrir una puerta, etc.

En la capa de red, estos dispositivos IoT integran protocolos y tecnologías de comunicación ya sea para interactuar entre sí (M2M) o para enviar y recibir datos hacia y desde la nube a través de internet.

Y en la capa de aplicación es donde se almacenan, procesan, se generan alertas, se visualizan los datos, etc, es decir que es la capa de donde se le da valor a los datos medidos u obtenidos a través de la capa de percepción. Si quieres obtener mas información sobre Internet de las Cosas te invito a leer el siguiente blog donde profundizo sobre IoT y muestro cómo implementar una arquitectura serverless para IoT: [https://dev.to/jhorvi24/iot-sin-servidores-en-aws-beneficios-de-los-servicios-serverless-para-iot-m66]

Tomando como base la anterior arquitectura de 3 capas, en el presente blog vamos a implementar cada una de esas capas de la siguiente forma:

  • Capa de percepción: usaremos un ESP32 con un sensor DHT11. El ESP 32 se caracteriza por su bajo costo y por tener integrado interfaces de comunicación inalámbricas como Bluetooth y WI-Fi.

esp32 with dht sensor

  • Capa de red: gracias al WI-Fi integrado del ESP32, podemos conectarnos a una red para enviar datos a un servidor local o directamente a un servidor en la nube a través de internet. WI-Fi es una tecnología de acceso a la red que transporta tráfico IP, operando en las capas 1 y 2 del modelo OSI. Para poder enviar los datos al servidor, ya sea local o en la nube, se utilizan protocolos de capa 7 como HTTP o MQTT. Aunque MQTT es más ligero y eficiente que HTTP, en la presente implementación trabajaremos con HTTP. A continuación, muestro parte del código usado en el ESP32:
void loop() 
{


  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi connection lost. Reconnecting...");
    WiFi.begin(ssid, password);
  }

  Serial.println("WiFi connected");
  Serial.println("Connected to IP address: ");
  Serial.println(WiFi.localIP()); 

  h = dht.readHumidity();
  t = dht.readTemperature();  

  Serial.print(F("Humidity: "));
  Serial.print(h);
  Serial.println("%");
  Serial.print(F("Temperature: "));
  Serial.print(t);
  Serial.println(F("°C "));

if (client.connect(server, httpPort)) {
  Serial.println("Connected to server");

  //Construir JSON
  String postData = "{\"humidity\":" + String(h) + ",\"temperature\":" + String(t) + "}"; 
  //String postData = "humidity=" + String(h) + "&temperature=" + String(t);  


  client.println("POST " + String(Endpoint) + " HTTP/1.1");  
  client.println("Host: " + String(server));  
  client.println("Content-Type: application/json");  
  client.println("Connection: close");  
  client.println("Content-Length: " + String(postData.length()));  
  client.println();  
  client.println(postData);  


  while (client.connected() || client.available()) {
    String line = client.readStringUntil('\n');
    if (line == "\r") {
      Serial.println("Headers received, response:");
      } else {
        Serial.println(line);
        }
   }
  client.stop();
  Serial.println("Data sent and connection closed");  

}else {
  Serial.println("Connection failed to server");
}
Enter fullscreen mode Exit fullscreen mode
  • Capa de aplicación: aquí es donde implementaremos el orquestador de contenedores en AWS. Antes de ir con la implementación, vale la pena destacar que AWS ofrece dos servicios de orquestación de contenedores:

    • Amazon Elastic Container service (Amazon ECS): orquestador nativo de AWS que se caracteriza por su simplicidad y fácil integración con otros servicios de AWS.
    • Amazon Elastic Kubernetes Service (Amazon EKS): basado en el proyecto de código abierto Kubernetes donde AWS administra parte de su arquitectura. Es la opción ideal cuando ya tienes un proyecto de kubernetes On-Premises y quieres migrar a la nube de AWS.

Para la implementación de la presente plataforma IoT vamos a usar el servicio ECS pero primero entendamos su arquitectura. En Amazon ECS se identifican los siguientes componentes:

  • Task Definition (Definición de tarea): permite configurar el contenedor que va a ejecutar un servicio específico. En ella se especifica:

    • Imagen del contenedor
    • El puerto
    • El sistema operativo
    • Los requisitos de CPU y memoria
    • Las variables de entorno
    • Dos roles de IAM importantes: el Task Role, que permite que la aplicación dentro del contenedor interactúe con otros servicios de AWS, y el T_ask Execution Role_, que opera a nivel de infraestructura y otorga permisos al agente de contenedores de Amazon ECS para ejecutar tareas como descargar imágenes o escribir logs. Todo esto se puede configurar desde la consola de AWS o mediante una plantilla en formato JSON.
  • Task (Tarea): es la unidad básica de ejecución en Amazon ECS. Se puede interpretar como una instancia de una Task Definition. Representa la ejecución real de la aplicación.

  • Services (servicios): es el componente que permite ejecutar y mantener un número definido de task (tareas). Es importante para lograr:

  • Alta Disponibilidad: si una task muere o no responde, el service lanza otra automáticamente. Se puede integrar con un balanceador de carga (Application Load Balancer/Network Load Balancer) para distribuir tráfico.

  • Alta Escalabilidad: se define cuántas replicas se quiere lanzar. Se puede integrar con un Auto Scaling Group.

  • Clúster: es un grupo lógico de recursos donde se ejecutan las tareas y servicios en la infraestructura de capacidad registrada en Amazon ECS.

El despliegue o lanzamiento de los contenedores se puede realizar en:

  • AWS Fargate: solución serverless donde no hay necesidad de administrar servidores, solo se define los recursos a nivel de las tasks y se paga por lo que se consume.

  • Amazon EC2: permite control sobre el tipo y cantidad de instancias (máquinas virtuales) que conforman el clúster.

Para entender mejor el funcionamiento de Amazon ECS me gusta hacer la analogía con el siguiente caso:

Pensemos en el servicio que ofrece un hotel cinco estrellas:

hotel example

  • El hotel tiene un director o administrador que sería Amazon ECS administrado por AWS.

  • Cada habitación tiene un manual de procedimiento que indica cómo debe estar organizada la habitación. Esto corresponde a una Task Definition (Definición de Tarea).

  • Una habitación ocupada sigue el manual de procedimiento (task definition). Para este caso la habitación ocupada corresponde a una task.

  • En la recepción debe haber alguien que se encargue que siempre haya habitaciones listas, por ejemplo debe asegurar que siempre haya 5 habitaciones listas, si una se desocupa o tiene un problema, prepara otra inmediatamente. Ese alguien de la recepción sería el Service (servicio).

  • El edificio físico del hotel es el Clúster _y su _capacidad, que corresponde a EC2 o Fargate, es el terreno o los cimientos sobre los que se construye.

Ya teniendo claro en qué consiste Amazon ECS, veamos los contenedores que vamos a implementar:

  • webserver-iot: servidor web desarrollado en Python con Flask, encargado de recibir los datos enviados por el ESP32 a través de HTTP, procesarlos y almacenarlos en la base de datos.

  • InfluxDB: base de datos de series de tiempo donde se almacenan los datos medidos por los sensores.

  • Grafana: herramienta de visualización donde configuraremos un dashboard para monitorear los datos de los sensores en tiempo real. Además, permite configurar alertas y notificaciones para actuar de forma proactiva ante cualquier anomalía detectada.

Con base en estos contenedores y en los requerimientos de una plataforma IoT, la arquitectura a implementar es la siguiente:

architecture design

Antes de ir con la implementación del orquestador de contenedores, analicemos cómo implementar el servidor web para IoT que después vamos a contenerizar.

webserver-iot

Para el servidor web IoT primero vamos a desarrollar la aplicación usando Python y el framework de desarrollo de aplicaciones web Flask. A continuación, comparto parte del código.

@app.route('/data', methods=['POST'])
def data_received():
    print("request arrive", flush=True )
    data = request.get_json()
    humidity = data['humidity']
    temperature = data['temperature']

    print(f"Humidity: {humidity}%, temperature: {temperature}°C")
    save_to_csv(temperature,humidity)
    save_to_influxdb(temperature,humidity)

    return 'Data received', 200


if __name__ == '__main__':
    print("Starting server...")
    app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False)
Enter fullscreen mode Exit fullscreen mode

Creamos el Dockerfile

FROM python:3.14-slim
WORKDIR /webserver-IoT
COPY requirements.txt /webserver-IoT/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY . /webserver-IoT
CMD ["python", "./server-iot.py"]
Enter fullscreen mode Exit fullscreen mode

Creamos la imagen Docker

docker build -t web-server-iot .

Como la idea es desplegar el contenedor a través del servicio Amazon ECS, subimos esta imagen desde mi repositorio local al servicio de Amazon ECR. Primero creamos el repositorio.

aws ecr create-repository --repository-name web-server-iot

Ya teniendo el repositorio creado realizamos un push de la imagen docker que tenemos local al repositorio Amazon ECR. El mismo servicio nos explica el paso a paso para realizar el push.

description to push the image to ecr


Implementación del servicio Amazon ECS

Usaremos Terraform para aprovisionar toda la infraestructura, que incluye la red, el clúster de Amazon ECS y la configuración de los services y task definitions. Siguiendo las buenas prácticas de IaC, configuraremos un backend remoto en Amazon S3 para almacenar el estado de Terraform y gestionar el lock de estado, evitando modificaciones concurrentes. El código está organizado por módulos, puedes encontrar el código completo en Github: github-aws-ecs-serverless , donde se configura:

  • El cluster de amazon ECS

En este módulo se define el nombre del cluster, las tasks definitions y los Services.

resource "aws_ecs_cluster" "IoT-cluster" {

  name = "IoT-cluster"

}
Enter fullscreen mode Exit fullscreen mode

Para el IoT-Cluster se configuran 3 Task Definitions y los respectivos Services:

  • Task definitions.

En la task definitions definimos la arquitectura de CPU, el sistema operativo sobre el cual van a correr las Tasks (tareas), la cantidad de CPU y memoria RAM que se reserva para que las diferentes Tasks se ejecuten, estos valores dependen de cuánto consume la aplicación o aplicaciones.

Y configuramos también las características del contenedor: nombre, URI de la imagen, puerto, variables de entorno (environment variables), entre otros. Dentro de una task definitions podemos configurar un único contenedor o varios, y esta decisión tiene implicaciones importantes.

Si en una misma task definitions lanzamos los 3 contenedores -web server IoT, InfluxDB y Grafana-, todos correrán sobre el mismo host, lo que significa que la comunicación se hace mediante localhost y el puerto correspondiente. El problema de este enfoque es que los contenedores quedan estrechamente acoplados: no pueden escalar de forma independiente, comparten recursos entre sí y pierden disponibilidad ya que, si un contenedor falla por alguna razón, lo más probable es que las tasks se caigan y por ende todos los contenedores.

Existe un caso específico donde si tiene sentido configurar mas de un contenedor por task definition: el Sidecar Pattern. En este patrón se tiene un contenedor principal y un contenedor auxiliar que le proporciona capacidades adicionales, como logging, monitoreo o seguridad, sin mezclar responsabilidades en el contenedor principal.
Para la presente implementación se configuraron 3 task definitions independientes lo que nos permite escalar, desplegar y monitorear cada servicio de forma autónoma, favoreciendo un entorno altamente disponible.

Las 3 task definitions son:

webserver-iot-task-definition

resource "aws_ecs_task_definition" "webserver-iot-task-definition" {

  family                   = "webserver-iot-taskdefinition-tf"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  memory                   = "3 GB"
  cpu                      = "1 vCPU"
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  container_definitions = jsonencode([
    {
      name  = "webserver-iot"
      image = "001239102331.dkr.ecr.us-east-1.amazonaws.com/web-server-iot:latest"
      environment = [
        {
          name  = "INFLUX_DB"
          value = "sensor-db"
        },
        {
          name  = "INFLUX_HOST"
          value = "http://influxdb-core.db.local:8181"
        },
        {
          name  = "INFLUX_TOKEN"
          value = ""
        }
      ]
      portMappings = [
        {
          containerPort = 5000
          hostPort      = 5000
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = "/ecs/webserver-iot"
          "awslogs-region"        = "us-east-1"
          "awslogs-stream-prefix" = "ecs"
          "awslogs-create-group": "true"
        }
      }
    }



  ])

}

Enter fullscreen mode Exit fullscreen mode

La imagen del contenedor se encuentra en le repositorio de ECR.

En esta task definition configuramos las siguientes variables de entorno:

  • INFLUX_HOST: corresponde al endpoint de InfluxDB. Como cada contenedor corre en su propia task deninitions, necesitamos un Service Discovery para que los servicios se puedan comunicar entre sí (mas adelante explicamos este concepto y cómo se configura). El valor de esta variable es la URL asignada por Service Discovery.

  • INFLUX_DB: nombre de la base de datos en InfluxDB

  • INFLUX_TOKEN: como InfluxDB está configurado en modo sin autenticación, esta variable se deja vacía. En un entorno productivo se recomienda configurar un token y asignarlo aquí para mejorar la seguridad.

influxdb-task-definition

resource "aws_ecs_task_definition" "influxdb-task-definition" {

  family                   = "influxdb-taskdefinition-tf"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  memory                   = "3 GB"
  cpu                      = "1 vCPU"
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  container_definitions = jsonencode([
    {
      name    = "influxdb-core"
      image   = "influxdb:3-core"
      command = ["influxdb3", "serve", "--node-id=my-node-0", "--object-store=file", "--data-dir=/var/lib/influxdb3/data", "--plugin-dir=/var/lib/influxdb3/plugins", "--without-auth"]
      portMappings = [
        {
          containerPort = 8181
          hostPort      = 8181
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = "/ecs/influxdb-core"
          "awslogs-region"        = "us-east-1"
          "awslogs-stream-prefix" = "ecs"
          "awslogs-create-group": "true"
        }
      }
    }



  ])

}
Enter fullscreen mode Exit fullscreen mode

Para esta tasks definitions usamos InfluxDB 3 cuya imagen del contenedor se descarga automáticamente desde Docker Hub. Como mencionamos anteriormente, InfluxDB permite configurar un token por seguridad y autenticación, pero para temas prácticos se va a dejar sin token. En la sección container_definitions, el parámetro _command _se configura de la siguiente forma:

command = ["influxdb3", "serve", "--node-id=my-node-0", "--object-store=file", "--data-dir=/var/lib/influxdb3/data", "--plugin-dir=/var/lib/influxdb3/plugins", "--without-auth"]

Destacamos el flag --without-auth, que es el que deshabilita la autenticación, y --data-dir, que define la ruta donde InfluxDB almacenará los datos dentro del contenedor.

grafana-task-definition

resource "aws_ecs_task_definition" "grafana-task-definition" {

  family                   = "grafana-taskdefinition-tf"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  memory                   = "3 GB"
  cpu                      = "1 vCPU"
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  container_definitions = jsonencode([
    {
      name  = "grafana-iot"
      image = "grafana/grafana-enterprise"
      portMappings = [
        {
          containerPort = 3000
          hostPort      = 3000
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = "/ecs/grafana-iot"
          "awslogs-region"        = "us-east-1"
          "awslogs-stream-prefix" = "ecs"
          "awslogs-create-group": "true"
        }
      }

    }



  ])

}
Enter fullscreen mode Exit fullscreen mode

La imagen del contenedor se descarga del DockerHub.

  • Services

Se configuran 3 Services para cada Task Definitions los cuáles son:

web-server-iot-services

resource "aws_ecs_service" "webserver-iot-service" {
  name            = "webserver-iot-service"
  cluster         = aws_ecs_cluster.IoT-cluster.id
  task_definition = aws_ecs_task_definition.webserver-iot-task-definition.arn
  desired_count   = 2
  launch_type     = "FARGATE"
  network_configuration {
    subnets         = [aws_subnet.subred-privada-A.id, aws_subnet.subred-privada-B.id]
    security_groups = [aws_security_group.webserver-iot-sg.id]
    assign_public_ip = false
  }
  load_balancer {
    target_group_arn = aws_lb_target_group.webserver-iot-tg.arn
    container_name   = "webserver-iot"
    container_port   = 5000
  }

}
Enter fullscreen mode Exit fullscreen mode
  • Se configuran dos réplicas, una por cada zona de disponibilidad, distribuidas en subredes privadas para mayor seguridad.

  • En network_configuration se asocian las dos subredes privadas y el grupo de seguridad correspondiente. Al estar en subredes privadas, las tareas no necesitan IP pública — el tráfico llega a través del ALB.

  • Se asocia un Application Load Balancer (ALB) para distribuir el tráfico entre las réplicas y garantizar alta disponibilidad, junto con su respectivo Target Group.

  • El launch_type se configura como Fargate (Serverless), lo que nos permite ejecutar contenedores sin administrar servidores ni planificar capacidad de infraestructura.

influxdb-service

resource "aws_ecs_service" "influxdb-service" {
  name            = "influxdb-service"
  cluster         = aws_ecs_cluster.IoT-cluster.id
  task_definition = aws_ecs_task_definition.influxdb-task-definition.arn
  desired_count   = 1
  launch_type     = "FARGATE"
  network_configuration {
    subnets         = [aws_subnet.subred-privada-AA.id]
    security_groups = [aws_security_group.db-sg.id]
  }

  service_registries {
    registry_arn = aws_service_discovery_service.influxdb-service-discovery.arn
  }

}
Enter fullscreen mode Exit fullscreen mode
  • Se configura una réplica en una subred privada, ya que la base de datos puede almacenar datos sensibles y no debe estar expuesta públicamente.

  • El tipo de lanzamiento se configura como Fargate, manteniendo el enfoque serverless de toda la plataforma.

  • Como cada contenedor tiene su propia task definition, necesitamos un mecanismo de red que permita la comunicación entre los diferentes servicios. Amazon ECS ofrece dos opciones para esto:

    • Service Connect: permite que las aplicaciones se comuniquen entre sí usando nombres abreviados y puertos estándar.
    • Service Discovery: se integra con AWS Cloud Map para mantener un nombre de host DNS que se resuelve en la dirección IP interna de cada tarea.

Para esta implementación optamos por Service Discovery, ya que nos proporciona un nombre DNS estable que el webserver-iot utiliza para conectarse a InfluxDB a través de la variable de entorno INFLUX_HOST.

resource "aws_service_discovery_private_dns_namespace" "db_local" {
  name        = "db.local"
  description = "Namespace for database services"
  vpc         = aws_vpc.iot-ecs-red.id
}

resource "aws_service_discovery_service" "influxdb-service-discovery" {
  name         = "influxdb-core"
  namespace_id = aws_service_discovery_private_dns_namespace.db_local.id

  dns_config {
    namespace_id   = aws_service_discovery_private_dns_namespace.db_local.id
    routing_policy = "MULTIVALUE"
    dns_records {
      ttl  = 10
      type = "A"
    }
  }


}
Enter fullscreen mode Exit fullscreen mode

grafana-service

resource "aws_ecs_service" "grafana-service" {
  name            = "grafana-service"
  cluster         = aws_ecs_cluster.IoT-cluster.id
  task_definition = aws_ecs_task_definition.grafana-task-definition.arn
  desired_count   = 1
  launch_type     = "FARGATE"
  network_configuration {
    subnets          = [aws_subnet.subred-privada-AA.id]
    security_groups  = [aws_security_group.grafana-sg.id]

  }
  load_balancer {
    target_group_arn = aws_lb_target_group.grafana-tg.arn
    container_name   = "grafana-iot"
    container_port   = 3000
  }

}

Enter fullscreen mode Exit fullscreen mode
  • Roles y políticas IAM

En este módulo se configura los roles de IAM necesarios para el funcionamiento de la plataforma:

Task role: permite que los contenedores que corren dentro de las tasks interactúen con otros servicios de AWS — por ejemplo, almacenar objetos en Amazon S3 o consultar credenciales en AWS Systems Manager Parameter Store. Para esta implementación lo dejamos sin configurar, ya que nuestros contenedores no necesitan interactuar con otros servicios de AWS.

Task execution role: permite al agente de ECS realizar operaciones de infraestructura como descargar imágenes desde Amazon ECR y escribir logs en CloudWatch. En Terraform lo definimos explícitamente con tres recursos: el rol con su política de confianza hacia ecs-tasks.amazonaws.com, la política administrada AmazonECSTaskExecutionRolePolicy y una política personalizada con permisos para crear y escribir logs en CloudWatch (logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents).

  • Infraestructura de red

En este módulo se configura la infraestructura de red que se basa en:

VPC

2 subredes públicas: una por zona de disponibilidad, donde se ubican los NAT Gateways y el Application Load Balancer que recibe el tráfico del ESP32.

3 subredes privadas: dos para las réplicas del webserver-iot, una por zona de disponibilidad, y una tercera compartida por InfluxDB y Grafana.

Internet Gateway: permite la comunicación entre la VPC e internet.

Route Tables: tablas de enrutamiento que dirigen el tráfico correctamente entre las subredes públicas y privadas.

2 NAT Gateways: uno por zona de disponibilidad, ubicados en las subredes públicas, permiten que los contenedores en subredes privadas salgan a internet sin estar expuestos públicamente — por ejemplo, para descargar imágenes desde Docker Hub o Amazon ECR.

- Balanceador de carga

En este módulo se configura el Application Load Balancer (ALB) que recibe el tráfico HTTP del ESP32 y lo distribuye entre las réplicas del webserver-iot.


resource "aws_lb" "alb-iot" {
  name               = "alb-iot"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb-iot-sg.id]
  subnets            = [aws_subnet.subred-publica-A.id, aws_subnet.subred-publica-B.id]

  tags = {
    Name = "alb-iot"
  }

}
Enter fullscreen mode Exit fullscreen mode

Para el Application Load Balancer se configuran dos target_group:

Uno para la comunicación con las réplicas del webserver-iot

resource "aws_lb_target_group" "webserver-iot-tg" {
  name        = "webserver-iot-tg"
  port        = 5000
  protocol    = "HTTP"
  vpc_id      = aws_vpc.iot-ecs-red.id
  target_type = "ip" #Obligatorio para fargate

  health_check {
    path                = "/health"
    interval            = 30
    timeout             = 3
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }

  tags = {
    Name = "target-group-iot"
  }

}
Enter fullscreen mode Exit fullscreen mode

Y otro para la comunicación con grafana

resource "aws_lb_target_group" "grafana-tg" {
  name        = "grafana-tg"
  port        = 3000
  protocol    = "HTTP"
  vpc_id      = aws_vpc.iot-ecs-red.id
  target_type = "ip" #Obligatorio para fargate

  health_check {
    path                = "/api/health"
    interval            = 30
    timeout             = 3
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }

  tags = {
    Name = "grafana-tg"
  }

}
Enter fullscreen mode Exit fullscreen mode

Con la arquitectura clara, procedemos a desplegar la infraestructura con Terraform. Solo necesitamos dos comandos:

terraform init

Inicializa el proyecto, descarga los providers necesarios y configura el backend remoto en S3. (el bucket s3 debe haberse creado previamente)

terraform apply

Crea todos los recursos definidos en los módulos. Terraform mostrará un resumen de los cambios antes de aplicarlos y pedirá confirmación.

Después que el aprovisionamiento de la infraestructura haya finalizado lo primero es verificar que el Service lance los tasks sin problema para cada task definitions y se encuentren en estado Runnig.

task running

Si las tasks se encuentran en el estado running se comprueba el acceso a Grafana a través del DNS del balanceador de carga

  • Por defecto el user y password son: admin

grafana dashboard

Se configura a InfluxDB como fuente de datos

influxdb as connection

Se configuran los respectivos parámetros:

URL: http://influxdb-core.db.local:8181
Database: sensor-db

parameters configurations

Para completar la configuración de InfluxDB como fuente de datos en Grafana, primero es necesario haber enviado al menos un dato desde el dispositivo IoT. Para ello, debemos actualizar el código del ESP32 reemplazando la dirección del servidor con el DNS del Application Load Balancer, el cual podemos obtener desde el output de Terraform o directamente desde la consola de AWS.

const char* server = "alb-iot-1024585334.us-east-1.elb.amazonaws.com";
const int httpPort = 80;
const char* Endpoint = "/data";
Enter fullscreen mode Exit fullscreen mode

Como el ESP32 envía datos de forma continua, podemos confirmar que el envío es exitoso monitoreando la salida desde el monitor serial del IDE de Arduino.

confirmatoin data sent

Ahora procedemos con la configuración del dashboard en grafana.

dashboard configuration

Para consultar los datos almacenados en InfluxDB desde Grafana utilizamos InfluxQL, el lenguaje de consulta propio de InfluxDB, cuya sintaxis es similar a SQL.

SELECT temperature, humidity FROM "values_sensor" WHERE "device" = 'esp32-home' AND time >= now() - 6h

grafana dashboard

Podemos cambiar la visualización de los datos

grafana dashboard

¡Y listo! Construimos una plataforma IoT production-ready sobre AWS: un ESP32 capturando datos de sensores, enviándolos a través de HTTP a un webserver-iot corriendo en Amazon ECS Fargate, almacenándolos en InfluxDB 3 y visualizándolos en tiempo real desde un dashboard de Grafana — todo aprovisionado con Terraform siguiendo buenas prácticas de Infraestructura como Código.
Este proyecto demuestra que es posible construir una arquitectura IoT escalable, disponible y serverless en AWS sin administrar un solo servidor. El código completo está disponible en GitHub.
En un próximo blog llevaremos esta arquitectura un paso más allá, reemplazando HTTP por MQTT para optimizar el consumo energético del dispositivo IoT.

Retos y mejoras

Lo interesante de construir proyectos propios es que te enfrentas a desafíos reales que se convierten en aprendizaje y experiencia. Estos son los principales retos encontrados y las mejoras identificadas para futuras iteraciones:

Alta disponibilidad

  • Si se despliegan múltiples tasks de Grafana en diferentes zonas de disponibilidad, es necesario configurar un mecanismo de persistencia compartida para que los datos de sesión no se pierdan entre réplicas.

  • Si se despliegan múltiples tasks de InfluxDB en diferentes zonas de disponibilidad, se debe configurar la replicación de datos y el failover automático para garantizar consistencia y disponibilidad.

Migración de HTTP a MQTT
Actualmente el ESP32 envía datos periódicamente al ALB a través de HTTP. Sin embargo, si el dispositivo opera con batería y se requiere optimizar el consumo energético, MQTT es una mejor alternativa: es un protocolo ligero diseñado precisamente para entornos con recursos limitados. El problema es que un ALB no soporta MQTT, ya que opera en capa 7 sobre HTTP/HTTPS. La solución es reemplazarlo por un NLB, que trabaja en capa 4 y permite tráfico MQTT.

Service Mesh
Actualmente la comunicación entre servicios se gestiona mediante Service Discovery con AWS Cloud Map. Sin embargo, a medida que el proyecto crezca y se añadan más servicios, será importante migrar hacia un Service Mesh, que ofrece capacidades avanzadas como observabilidad del tráfico, balanceo de carga entre servicios y políticas de seguridad a nivel de red.

Top comments (0)