ESP32 AWS IoT Core Lambda DynamoDB 연동 파이프라인 구축 배경
ESP32 마이크로컨트롤러 기반 IoT 시스템에서 센서 데이터(온도, 습도 등)를 클라우드로 전송할 때, 단순 HTTP REST API 호출 방식은 지속적인 연결 오버헤드와 전력 소모를 유발합니다.
이러한 문제를 해결하기 위해 저전력 Pub/Sub 구조의 MQTT 프로토콜과 AWS IoT Core, Serverless 인프라(AWS Lambda, Amazon DynamoDB)를 연동하는 아키텍처가 널리 사용됩니다.
그러나 기존 구현 가이드에서는 다음과 같은 기술적 오류가 빈번하게 발생합니다.
- ESP32 C 코드에서 C-SDK 레거시 API와 ESP-IDF esp_mqtt_client API의 혼용
- JSON 포맷 생성 시 sprintf 사용으로 인한 Buffer Overflow 위험
- AWS IoT Rule SQL 및 AWS Lambda Event Payload 구조(SQS/Kinesis 래핑 여부) 불일치로 인한 KeyError 발생
본 포스팅에서는 ESP-IDF v5.x 환경에서 esp_mqtt_client와 cJSON 라이브러리를 사용해 TLS v1.2 mTLS 통신을 안전하게 수행하고, AWS IoT Rules Engine을 거쳐 AWS Lambda 및 DynamoDB로 이어지는 무결성 높고 안정적인 IoT 데이터 파이프라인을 구축합니다.
ESP32 AWS IoT Core DynamoDB 파이프라인 핵심 요약
- ESP32 ESP-IDF esp_mqtt_client 및 cJSON 적용: cJSON 기반의 안전한 JSON 직렬화 및 FreeRTOS esp_mqtt_client를 통해 iot/temperature 토픽으로 TLS 8883 포트 기반 mTLS 메시지를 발행합니다.
- AWS IoT Rules Engine 직접 라우팅: SQL SELECT * FROM 'iot/temperature' 구문을 이용해 수신된 MQTT 메시지를 AWS Lambda로 직접 전달합니다.
- AWS Lambda & boto3 DynamoDB put_item 최적화: Lambda Handler로 직접 전달되는 Event Payload 구조에 맞춰 boto3 put_item API를 호출하고 Amazon DynamoDB에 데이터를 영속화합니다.
ESP32 ESP-IDF 및 AWS IoT Core DynamoDB 파이프라인 상세 분석 및 구현
AWS IoT Core 데이터 흐름 및 구성 요소 비교
| 구성 요소 (Component) | 주요 프로토콜 및 API (Protocol & API) | 핵심 기능 (Role & Function) | 비고 및 데이터 형태 (Data Format) |
| ESP32 MCU | MQTT over TLS v1.2 (Port 8883) | ADC 센서 데이터 측정 및 MQTT Publish | {"device_id": "ESP32_001", "temperature": 25.50} |
| AWS IoT Core | Rules Engine SQL | MQTT 메시지 수신 및 AWS 서비스 라우팅 | SELECT * FROM 'iot/temperature' |
| AWS Lambda | Python 3.x (boto3) | Payload 파싱 및 DynamoDB 데이터 삽입 | Event JSON 직렬 데이터 수신 |
| Amazon DynamoDB | NoSQL (put_item) | 시계열 센서 데이터 영속화 | Primary Key: device_id (Partition), timestamp (Sort) |
ESP32 ESP-IDF C 코드 구현 (cJSON 및 esp_mqtt_client)
기존 sprintf 및 비표준 API 사용 방식을 지양하고, ESP-IDF의 esp_mqtt_client.h와 cJSON.h를 활용하여 안전하게 구현합니다.
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "mqtt_client.h"
#include "cJSON.h"
static const char *TAG = "AWS_IOT_MAIN";
static esp_mqtt_client_handle_t client = NULL;
#define AWS_IOT_ENDPOINT "your-iot-endpoint-ats.iot.ap-northeast-2.amazonaws.com"
#define AWS_IOT_TOPIC "iot/temperature"
/* Client Certificate and Private Key embedded via binary */
extern const uint8_t aws_root_ca_pem_start[] asm("_binary_aws_root_ca_pem_start");
extern const uint8_t certificate_pem_crt_start[] asm("_binary_certificate_pem_crt_start");
extern const uint8_t private_pem_key_start[] asm("_binary_private_pem_key_start");
void publish_temperature(float temperature) {
if (client == NULL) {
ESP_LOGE(TAG, "MQTT client is not initialized");
return;
}
/* Build JSON Payload safely using cJSON */
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "device_id", "ESP32_001");
cJSON_AddNumberToObject(root, "temperature", temperature);
char *payload = cJSON_PrintUnformatted(root);
int msg_id = esp_mqtt_client_publish(client, AWS_IOT_TOPIC, payload, 0, 1, 0);
ESP_LOGI(TAG, "Published message, msg_id=%d, payload=%s", msg_id, payload);
cJSON_free(payload);
cJSON_Delete(root);
}
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
esp_mqtt_event_handle_t event = event_data;
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED to AWS IoT Core");
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
break;
default:
break;
}
}
void aws_iot_demo_init(void) {
esp_mqtt_client_config_t mqtt_cfg = {
.broker = {
.address = {
.uri = "mqtts://" AWS_IOT_ENDPOINT ":8883",
},
.verification = {
.certificate = (const char *)aws_root_ca_pem_start,
},
},
.credentials = {
.authentication = {
.certificate = (const char *)certificate_pem_crt_start,
.key = (const char *)private_pem_key_start,
},
},
};
client = esp_mqtt_client_init(&mqtt_cfg);
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_mqtt_client_start(client);
}
void app_main(void) {
aws_iot_demo_init();
float dummy_temperature = 25.5f;
while (1) {
vTaskDelay(pdMS_TO_TICKS(10000));
publish_temperature(dummy_temperature);
dummy_temperature += 0.1f;
}
}
AWS Lambda Python boto3 구현 (DynamoDB put_item)
AWS IoT Core Rules Engine에서 Lambda를 직접 타겟으로 지정한 경우, event 객체에 MQTT JSON Payload가 직접 딕셔너리 형태로 전달됩니다. Records 및 body 래핑 구조를 사용하면 KeyError가 발생하므로 아래와 같이 작성해야 합니다.
import time
import logging
import boto3
from decimal import Decimal
logger = logging.getLogger()
logger.setLevel(logging.INFO)
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('TemperatureData')
def lambda_handler(event, context):
logger.info("Received event: %s", event)
try:
device_id = event.get('device_id', 'UNKNOWN_DEVICE')
temperature_raw = event.get('temperature')
if temperature_raw is None:
raise ValueError("Missing 'temperature' field in payload")
# Convert float to Decimal for DynamoDB Compatibility
temperature = Decimal(str(temperature_raw))
timestamp = int(time.time())
table.put_item(
Item={
'device_id': device_id,
'timestamp': timestamp,
'temperature': temperature
}
)
logger.info("Successfully inserted item for device %s", device_id)
return {
'statusCode': 200,
'body': 'Data successfully stored in DynamoDB'
}
except Exception as e:
logger.error("Error processing event: %s", str(e))
raise e
ESP32 AWS IoT 디버깅 및 트러블슈팅 팁
1. AWS IoT Core MQTT 테스트 클라이언트 검증
ESP32 디바이스를 클라우드에 연결하기 전, AWS IoT 콘솔의 MQTT 테스트 클라이언트를 활용해 통신 경로를 사전 검증하세요.
- 주제 구독: iot/temperature
- 디바이스에서 전송한 JSON 데이터가 동일하게 수신되는지 확인하여 ESP32 네트워크 연결을 격리 분석합니다.
2. CMakeLists.txt 인증서 임베딩 설정
ESP-IDF 환경에서 PEM 인증서 파일을 바이너리에 직접 포함하려면 main/CMakeLists.txt 파일에 아래 구문을 추가해야 합니다.
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
EMBED_TXTFILES "certs/aws_root_ca.pem"
"certs/certificate.pem.crt"
"certs/private.pem.key")
ESP32 AWS IoT Core 개발 시 흔히 하는 실수 및 수정법
1. Lambda Event 구조 오해로 인한 KeyError
- 증상: AWS Lambda 실행 로그(CloudWatch Logs)에서 KeyError: 'Records' 발생.
- 원인: SQS/Kinesis/SNS 트리거용 코드(event['Records'])를 AWS IoT Core Rules Engine 직송 이벤트에 적용했기 때문입니다.
- 해결 방법: Rules Engine이 전달하는 Payload는 JSON 딕셔너리 형태이므로 event.get('temperature') 형태로 직접 접근하세요.
2. DynamoDB Float Type Mismatch (TypeError)
- 증상: Lambda에서 DynamoDB put_item 호출 시 TypeError: Float types are not supported. Use Decimal instead. 예외 발생.
- 원인: Python boto3 SDK는 DynamoDB 데이터 타입 변환 시 Native Python float 데이터 타입을 지원하지 않습니다.
- 해결 방법: from decimal import Decimal 모듈을 가져온 뒤 Decimal(str(val)) 형태로 변환하여 저장하세요.
3. ESP32 Stack Overflow 패닉 발생
- 증상: ESP32 실행 중 Guru Meditation Error: Core 1 panic'ed (Unhandled debug exception) 또는 Stack overflow 발생 후 리셋.
- 원인: FreeRTOS 태스크 스택 메모리가 부족하거나 cJSON 할당 해제 누락으로 인한 Memory Leak 때문입니다.
- 해결 방법: xTaskCreate 시 태스크 스택을 최소 8192 바이트(8KB) 이상 할당하고, cJSON_Delete() 및 cJSON_free()를 명시적으로 호출해 메모리를 해제하세요.
ESP32 AWS IoT Core Lambda DynamoDB 파이프라인 요약
본 포스팅에서는 ESP32 ESP-IDF 환경에서 esp_mqtt_client와 cJSON을 사용하여 AWS IoT Core로 보안 MQTT 메시지를 발행하고, Rules Engine과 AWS Lambda를 통해 Amazon DynamoDB에 센서 데이터를 안전하게 영속화하는 표준 파이프라인을 구축했습니다.
코드 내 Event 구조 매핑, DynamoDB Decimal 타입 변환, ESP-IDF 메모리 관리를 엄격히 적용하면 프로덕션 환경에서도 높은 안정성을 유지하는 IoT 시스템을 구현할 수 있습니다.