DEV Community

Ramses
Ramses

Posted on

AWS IoT Core Starter with Esp32, MQTT, Rust & Terraform

Introduction

Most ESP32 + AWS IoT Core examples/tutorials use the Arduino framework and rely on manually clicking through the AWS console. That approach works for quick demos, but it quickly becomes confusing and error-prone—especially if you need to reproduce the setup or scale to multiple devices.

I recently created a repository that makes it much easier to get ESP32 devices securely talking to AWS IoT Core with Infrastructure-as-Code, and modern firmware written in Rust using the Espressif ESP-IDF framework.

Prerequisites

  • The tools needed to get this example working are described in the repository
  • Basic understanding of the MQTT protocol
  • Basic understanding of terraform
  • Clone the repository
git clone https://github.com/RamMaths/aws-iot-esp32-example.git 
Enter fullscreen mode Exit fullscreen mode

What we are going to build

Instead of another “hello world” demo, the goal here is to create a minimal but extensible template for ESP32 devices communicating with AWS IoT Core.

Step 1: Subscribe to Topics

In MQTT, a topic works like a communication channel between devices and services.

  • ESP32 devices subscribe to the topic: esp32/1
  • AWS IoT Core Test Client subscribes to the topic: esp32/2

This setup ensures that:

  • Commands sent from the test client on esp32/1 will be received by the ESP32 devices.
  • Device responses published to esp32/2 will be received by the test client.

Step 2: Publish a Command from the Test Client

Next, we use the AWS IoT Test Client to publish a message in JSON format to the topic esp32/1.

{ "message": "ping" } 
Enter fullscreen mode Exit fullscreen mode
  • The message field contains the command we want the ESP32 device to execute.
  • In this example, we send "ping".
  • The firmware listens for this value and matches it against predefined actions (e.g., respond with "pong").

Using JSON keeps the communication simple and extensible—you can later add more fields or new commands without changing the overall structure.

Step 3: Device Response

When an ESP32 device receives a command, the firmware matches it against predefined actions.

For the ping command, the device responds by publishing:

{ "message": "pong" } 
Enter fullscreen mode Exit fullscreen mode

to the topic esp32/2.
Since the test client is subscribed to this topic, it immediately receives the response.

🔧 This example only implements a simple ping → pong workflow, but this project is designed to be extensible. For example, if a device subscribed to esp32/light, you could add commands like:

{ "message": "on" } { "message": "off" } 
Enter fullscreen mode Exit fullscreen mode

Acquiring cloud resources

With the repository cloned and the terraform CLI installed, we can now provision the required AWS IoT Core resources.

1. Initialize Terraform

Navigate into the Terraform directory and initialize the project:

cd terraform terraform init 
Enter fullscreen mode Exit fullscreen mode

👉 Make sure your AWS credentials are configured (aws configure) before proceeding.

2. Configure Variables

Copy the example variables file:

cp terraform.tfvars.example terraform.tfvars 
Enter fullscreen mode Exit fullscreen mode

Open terraform.tfvars and edit the things array. Each thing represents an IoT device in AWS IoT Core. For each device, you define:

  • name: the device identifier in AWS IoT
  • topic_prefix: the MQTT topic namespace the device can publish/subscribe to

Example configuration for two ESP32 devices (S3 and C3):

# Terraform variables for AWS IoT ESP32 Example # Copy this file to terraform.tfvars and customize the values # List of IoT Things to create with their configurations things = [ { name = "esp32s3" topic_prefix = "esp32" }, { name = "esp32c3" topic_prefix = "esp32" } ] # AWS region where IoT resources will be created region = "us-east-1" # Tags to apply to all AWS resources tags = { Project = "ESP32-IoT-Example" Environment = "development" ManagedBy = "terraform" Owner = "your-name" } 
Enter fullscreen mode Exit fullscreen mode

3. Apply Terraform

Finally, deploy the resources:

terraform plan terraform apply 
Enter fullscreen mode Exit fullscreen mode

This will create:

  • IoT Things for each device (esp32s3, esp32c3)
  • Certificates and policies (automatically downloaded into certs/)
  • Topic permissions based on the prefixes you specified

✅ At this point, your AWS environment is fully provisioned and ready for your ESP32 devices to connect securely.

Compiling the Firmware

With the cloud resources provisioned by Terraform, the next step is to configure, build, and flash the ESP32 firmware.

1. Configure the Project

Navigate into the firmware directory for your ESP32 variant:

After terraform creates the resources it will output something like this

cd firmware/example 
Enter fullscreen mode Exit fullscreen mode

(See the README setup guide for details on configuring Xtensa vs RISC-V targets.)

2. Create cfg.toml

Terraform outputs the connection details you need. Your config should look like this:

[example] # Wi-Fi Configuration wifi_ssid = "YOUR_WIFI_NETWORK" wifi_pass = "YOUR_WIFI_PASSWORD" mqtt_url = "mqtts://your-endpoint.iot.region.amazonaws.com" mqtt_client_id = "thing_name" mqtt_topic_pub = "topic_prefix" mqtt_topic_sub = "topic_prefix" cert_ca = "certs/AmazonRootCA1.pem" cert_crt = "certs/[certificate-id]-certificate.pem.crt" cert_key = "certs/[certificate-id]-private.pem.key" 
Enter fullscreen mode Exit fullscreen mode
  • Replace wifi_ssid and wifi_pass with your network credentials.
  • Set mqtt_topic_pub = "esp32/2" and mqtt_topic_sub = "esp32/1".
  • Leave the rest as generated by Terraform.

Save this as cfg.toml inside your firmware project.

3. Copy Certificates

Copy the certificates for your device into the firmware directory. The folder must be named certs:

cp -r ../../terraform/certs/thing_name ./certs 
Enter fullscreen mode Exit fullscreen mode

4. Build and Flash

Now compile and flash the firmware to your ESP32:

cargo build --release cargo espflash ./path/to/the/binary -p /path/to/your/esp32/port cargo espmonitor /path/to/your/esp32/port 
Enter fullscreen mode Exit fullscreen mode

What to Expect in the Logs

Once the ESP32 connects to Wi-Fi and establishes an MQTT session with AWS IoT Core, you should see logs similar to the following:

I (1234) example: WiFi connected successfully I (1235) example: MQTT client created successfully I (1236) example: Subscribed to topic "esp32/1" I (1237) example: Starting main application loop 
Enter fullscreen mode Exit fullscreen mode

These messages confirm that:

  • Your device joined the Wi-Fi network
  • The MQTT client was initialized correctly
  • The subscription to the expected topic succeeded
  • The application is now running and ready to handle messages

Common Errors

It’s normal to briefly see messages like:

Failed to subscribe to topic "esp32/1": {}, retrying... 
Enter fullscreen mode Exit fullscreen mode

This happens while AWS IoT validates the certificates and sets up the secure connection. As long as the retries eventually succeed and you see the subscription message, your setup is working correctly.

🎉 Congratulations!

If you’ve followed along to this point, you’ve come a long way — from setting up cloud resources with Terraform, to configuring certificates, flashing firmware, and seeing your ESP32 securely connect to AWS IoT Core. That’s a huge achievement! 🚀

Thank you for reading and coding along. I hope you not only got things working but also learned something new about how ESP32, MQTT, and AWS IoT fit together.

🧑‍💻 What’s Next? Experiment!

This repository is meant as a starting point. Feel free to dive in and make it your own:

  • Add more commands beyond the simple ping → pong demo
  • Explore how the Wi-Fi connection is established in startup.rs
  • Take a closer look at our generalized MQTT client in client.rs
    • It runs a separate thread to keep listening for messages
    • Uses crossbeam_channel to forward messages to the main thread safely
  • Experiment with publishing structured JSON commands or even sensor data

🔧 Extending the Main Loop

Inside main.rs, you’ll find the main loop where incoming MQTT messages are matched against commands. Right now it only handles "ping". Here’s where you can extend it:

match msg.message.as_str() { "ping" => { info!("Ping received, sending pong"); JsonMessage { message: format!("pong from: {}", app.config.mqtt_client_id), } } "light_on" => { info!("Turning light ON"); JsonMessage { message: "Light is now ON".to_string(), } } "light_off" => { info!("Turning light OFF"); JsonMessage { message: "Light is now OFF".to_string(), } } _ => { warn!("Unknown action: {}", msg.message); JsonMessage { message: format!("Unknown action: {}", msg.message), } } } 
Enter fullscreen mode Exit fullscreen mode

You can use this pattern to hook up any device action — toggling GPIO pins, controlling peripherals, or sending structured responses back to AWS IoT Core.

🙌 Thank You

Thanks again for reading. Now go ahead — experiment, break things, fix them again, and build something awesome. That’s where the real learning happens.

If you found this helpful:

  • ⭐ Star the repository on GitHub to support the project
  • 💬 Share your feedback, questions, or improvements in the repo’s Discussions/Issues
  • 🚀 Show off what you’ve built with this template — I’d love to see your projects!

Top comments (0)