Route IoT Telemetry to Timestream
Route synthetic vital-sign MQTT data to Amazon Timestream with Terraform.
Introduction
30 Second Summary
A live health reading can flash across a screen before disappearing when the page closes. A useful cloud platform preserves that reading for questions that come later.
In this project, you will use Terraform to route synthetic vital signs from AWS IoT Core over MQTT into Amazon Timestream for LiveAnalytics. The result is durable telemetry history that you can query by device.
What You'll Build
You'll publish a synthetic vital-sign message from a simulated device before watching its time-stamped measures appear in a device-scoped query.
By the end of this project, you'll have:
- A live MQTT telemetry demo showing a synthetic vital-sign message on the medical/vitals/+ subscription after publication to medical/vitals/device-001.
- A repeatable infrastructure deployment that recreates the durable telemetry path from Terraform files.
- A queryable vital-sign history where three readings become nine time-stamped measures scoped to device-001.
- Secret Mission: Publish a second simulated device to the shared table. Prove that its readings remain independently queryable.
Are there any prerequisites?
You need a Windows device with Visual Studio Code, the AWS CLI, Terraform 1.16.2, and access to an AWS sandbox that can manage AWS IoT Core, Amazon Timestream for LiveAnalytics, IAM, and STS. Use synthetic data only so patient names, identifiers, and protected health information stay out of this lab.
Before We Start
Before the hands-on work begins, lock in your plan to route synthetic vital-sign messages from AWS IoT Core through MQTT into Amazon Timestream for LiveAnalytics. Durable queryable history supports the larger cloud platform while patient data stays outside this lab.
Verify Your Windows Lab Environment
Your synthetic telemetry lab depends on local tools that can describe its cloud resources. The active cloud identity determines whether those resources can be created.
This project pins Terraform to 1.16.2. A different version can surface incompatible behavior halfway through deployment.
The AWS CLI uses the credentials active on your machine. The identity check reveals the account that Terraform would use.
In this step, get ready to:
- Create an empty Visual Studio Code workspace for the telemetry project.
- Confirm that Terraform reports version 1.16.2.
- Confirm authenticated AWS CLI access from the caller identity output.
Create the Visual Studio Code workspace
A Visual Studio Code workspace keeps the project files beside the terminal. Opening the terminal from that workspace also sets its starting folder correctly.
- Press the Windows key to open Search.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder....
- Select Desktop in the folder picker.
- Click New Folder.
- Enter medical-iot-telemetry as the folder name.
- Click Select Folder.
That is your workspace ready. You should see medical-iot-telemetry at the top of the Explorer sidebar.
- Click View in the top menu bar.
- Click Terminal.
You should see an integrated terminal at the bottom of Visual Studio Code. Its prompt starts inside the medical-iot-telemetry workspace.
Verify the Terraform version
Terraform reads the infrastructure configuration that you create later. The version check prints the installed binary version plus any installed plugins.
- Check the installed Terraform version by running this command:
terraform version
What does this command do?
The command reports the Terraform binary version available in this terminal. It also lists installed provider plugins when any are present.
✔️ I see the required version
Your first tool check passes. You should see Terraform v1.16.2 near the top of the output.
ⓧ I see an older version
The installed Terraform version does not match this project's pin. Update it before continuing so the later configuration uses the expected CLI behavior.
- Open HashiCorp's official Terraform install page.
- Download Terraform 1.16.2 from the Windows AMD64 section.
- Complete the official Windows installation instructions on that page.
- Restart Visual Studio Code so it reloads the updated tool path.
- Return to the medical-iot-telemetry workspace from earlier.
- Return to the integrated terminal from earlier.
- Repeat the version check shown above.
ⓧ Command not found
Windows cannot currently locate the Terraform executable. Install the pinned version before continuing.
- Open HashiCorp's official Terraform install page.
- Download Terraform 1.16.2 from the Windows AMD64 section.
- Complete the official Windows installation instructions on that page.
- Restart Visual Studio Code so it reloads the updated tool path.
- Return to the medical-iot-telemetry workspace from earlier.
- Return to the integrated terminal from earlier.
- Repeat the version check shown above.
Still not seeing Terraform v1.16.2?
A terminal opened before the installation can keep the old tool path. Restart Visual Studio Code before checking the version again.
Still stuck? Help me troubleshoot why my Windows terminal does not report Terraform v1.16.2. If needed, share the non-secret command output with the NextWork community.
Verify your authenticated AWS identity
The caller identity check shows which AWS account receives requests from this terminal. It also identifies the active user or role.
Before you run this check, which AWS account do you expect the terminal to report?
- Check the active AWS identity by running this command:
aws sts get-caller-identity
What does this command show?
- The Account value identifies the active AWS account.
- The Arn value identifies the active AWS user or role.
You should see output containing both Account and Arn values. That result confirms the AWS CLI is authenticated for the infrastructure work ahead.
Missing the AWS identity values?
- Refresh your sandbox credentials through its approved sign-in process.
- Run the identity check again after refreshing the credentials.
- Stop if the output points to an AWS account that you did not intend to use.
Need another pair of eyes? Help me troubleshoot why my AWS caller identity check is not returning the expected Account and Arn values. If needed, share only the non-secret error details with the NextWork community.
You have cleared the checks that could derail the deployment later. Next, you will publish a synthetic vital-sign message through AWS IoT Core.
Publish a Live Vital-Signs Message
Your Windows lab is ready. The verified Terraform version gives you a stable starting point for the first telemetry test.
The AWS IoT Core MQTT test client in the AWS Management Console lets you watch a synthetic reading arrive live. You'll also test whether anything remains after you leave the page.
A live arrival proves the ingestion edge accepts the message. Persistence is a separate requirement because the larger platform needs queryable history.
In this step, get ready to:
- Subscribe to medical/vitals/+ in the AWS IoT Core MQTT test client.
- Publish a synthetic vital-sign reading to medical/vitals/device-001.
- Test whether the live subscription and message log provide durable history.
Subscribe to the vital-signs topic
A topic filter selects which MQTT messages the client receives. The + wildcard matches exactly one topic level beneath medical/vitals/.
Your browser session uses a separate sign-in from the AWS CLI identity you verified earlier.
- Sign in to the AWS Management Console with your sandbox account or approved role.
- Set the console Region to US East (N. Virginia) in the top navigation bar.
- Use the search box in the console header to open AWS IoT Core.
- Choose Test in the left menu.
- Choose MQTT test client.
- Choose Subscribe to a topic.
- Enter medical/vitals/+ in the Topic filter field.
- Choose Subscribe.
You should see medical/vitals/+ in the Subscriptions list. Your wildcard listener is ready for a one-level device ID.
Publish the synthetic reading
Each payload key represents one synthetic vital sign. The mix of whole numbers and a decimal keeps each reading's intended numeric shape.
Keep the Lab Synthetic
- Use only synthetic device IDs and vital-sign values.
- Keep patient names, identifiers, and protected health information out of every topic and payload.
- Choose Publish to a topic.
- Enter medical/vitals/device-001 in the Topic name field.
- Copy the synthetic reading into the message payload window:
{
"heart_rate_bpm": 72,
"spo2_percent": 98,
"temperature_c": 36.8
}
What Does This Payload Represent?
- The heart_rate_bpm field carries the synthetic heart-rate reading as a whole number.
- The spo2_percent field carries the synthetic oxygen-saturation reading as a whole number.
- The temperature_c field carries the synthetic temperature as a decimal value.
Before you publish, do you think the + wildcard will match device-001 as one topic level?
- Choose Publish.
You should see the matching JSON payload under the medical/vitals/+ subscription. You have proved that AWS IoT Core can receive the synthetic device reading live.
Don't See the Message?
- Check that the Topic name is exactly medical/vitals/device-001 because MQTT topics are case-sensitive.
- Confirm that the Subscriptions list still contains medical/vitals/+.
- Confirm that the console still shows US East (N. Virginia).
Still stuck? Help me troubleshoot why my MQTT message is missing.
Test whether the live log persists
A live message log answers whether delivery is happening now. Durable history must remain available after the client session changes.
Before you leave the page, do you think the subscription and message log will still be there when you return?
- Leave the MQTT test client by selecting another AWS IoT Core page from the left navigation pane.
- Return to Test in the left navigation pane.
- Choose MQTT test client.
You'll see that the client did not retain the medical/vitals/+ subscription or its message log. You found the exact gap this project is built to close.
Your ingestion edge accepts live MQTT messages, but the test client provides no durable queryable history. Next, you'll use Terraform to provision a persistent time-series path in Amazon Timestream for LiveAnalytics.
Provision the Persistent Telemetry Path
Your live MQTT test proved that AWS IoT Core can receive synthetic vital signs. The test client also exposed the gap in your pipeline because its message log disappears when you leave the page.
You now need durable time-series storage that can preserve each reading for later queries. In this step, Terraform provisions the complete route into Amazon Timestream for LiveAnalytics without adding a Lambda intermediary.
In this step, get ready to:
- Pin the Terraform CLI version and AWS provider version.
- Define the Timestream storage resources and least-privilege IAM access.
- Deploy the AWS IoT Core rule and confirm its output values.
Pin the Terraform configuration
Terraform version constraints keep your local CLI and provider behavior consistent with this project. The region variable gives every AWS resource one shared destination.
- Create versions.tf in the Visual Studio Code Explorer sidebar with this configuration:
terraform {
required_version = "= 1.16.2"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.64.0"
}
}
}
What does this configuration do?
- The required_version constraint requires Terraform 1.16.2 for this project.
- The required_providers block selects hashicorp/aws as the provider source.
- The provider constraint pins the AWS provider to 6.64.0.
- Save versions.tf.
- Initialize the medical-iot-telemetry folder by running this command in the integrated terminal:
terraform init
What does this command do?
The command initializes the current folder for Terraform. It installs the required provider in .terraform and records the selected provider version in .terraform.lock.hcl.
You should see output confirming that the folder was initialized. Your Visual Studio Code Explorer should also list .terraform.lock.hcl.
Initialization failed?
- Confirm that the terminal path ends with medical-iot-telemetry.
- Confirm that versions.tf contains matching braces and quotes.
- Check your network connection because Terraform needs to download the AWS provider.
Still stuck? Help me troubleshoot Terraform initialization in my medical-iot-telemetry folder.
- Create variables.tf in the Visual Studio Code Explorer sidebar with this configuration:
variable "aws_region" {
description = "AWS Region for the medical IoT telemetry lab"
type = string
default = "us-east-1"
}
How does the region variable help?
The aws_region variable stores the deployment region in one place. Its default keeps the lab in us-east-1.
- Save variables.tf.
- Validate the two configuration files by running:
terraform validate
What does validation check?
Terraform checks whether the initialized configuration is syntactically valid and internally consistent. This catches structural mistakes before any AWS resources are created.
You should see confirmation that the configuration is valid. Your pinned versions and region variable are now ready for the infrastructure definitions.
Configuration not valid?
- Compare the braces in versions.tf with the reference above.
- Check that variables.tf uses the exact variable name aws_region.
Need another pair of eyes? Help me find the validation problem in my Terraform version and region files.
Define storage and least-privilege access
The telemetry path needs a database for organization and a table for time-stamped measures. Its IAM role also needs enough permission to write records without gaining broad access to other Timestream tables.
- Create main.tf in the Visual Studio Code Explorer sidebar with this foundation:
provider "aws" {
region = var.aws_region
}
data "aws_caller_identity" "current" {}
resource "aws_timestreamwrite_database" "medical_iot" {
database_name = "medical_iot"
}
What does this foundation define?
- The AWS provider reads aws_region so the configuration deploys into us-east-1.
- The caller identity data source retrieves the authenticated AWS account ID for the trust policy.
- The database resource creates the medical_iot container for the telemetry table.
- Save main.tf.
- Check the storage foundation by running:
terraform validate
What does this check prove?
A successful validation proves that the provider can resolve the region variable. It also confirms that the caller identity data source and database resource use valid references.
You should see confirmation that the configuration is valid. Terraform can now connect the table to the database.
Foundation validation failed?
- Check that the provider references var.aws_region exactly.
- Check that the database resource is named medical_iot in both places.
Still seeing a problem? Help me troubleshoot the provider, caller identity, and Timestream database definitions.
- Add the Timestream table below the database resource in main.tf by copying this code:
resource "aws_timestreamwrite_table" "vitals" {
database_name = aws_timestreamwrite_database.medical_iot.database_name
table_name = "vitals"
retention_properties {
memory_store_retention_period_in_hours = 1
magnetic_store_retention_period_in_days = 1
}
}
How does the table preserve telemetry?
- The table references the database resource directly. This reference lets Terraform infer that the database must exist first.
- The table keeps one hour of data in the memory store for recent access.
- The table keeps one day of data in the magnetic store for durable history.
- Save main.tf.
- Check the table definition by running:
terraform validate
What does this check prove?
A successful result confirms that the table points to the database correctly. It also confirms that both retention values are accepted by the provider schema.
You should see confirmation that the configuration is valid. The storage layer now has a database and a table definition.
Table validation failed?
- Confirm that database_name references aws_timestreamwrite_database.medical_iot.database_name.
- Confirm that the memory retention is 1 hour.
- Confirm that the magnetic retention is 1 day.
Need help comparing the resource? Help me fix my Timestream table and retention configuration.
AWS IoT Core assumes a role when its rule writes into Timestream. The trust policy limits that assumption to the AWS account returned by the caller identity data source.
- Add the IoT rule role below the table resource in main.tf by copying this code:
resource "aws_iam_role" "iot_timestream" {
name = "medical-iot-timestream-rule-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "iot.amazonaws.com"
}
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}
]
})
}
How is the role scoped?
- The service principal allows iot.amazonaws.com to assume the role.
- The sts:AssumeRole action defines the permitted trust operation.
- The aws:SourceAccount condition restricts role assumption to your current AWS account.
- The jsonencode function converts the Terraform value into the JSON trust policy that IAM expects.
- Save main.tf.
- Check the account-scoped trust policy by running:
terraform validate
What does this check prove?
A successful validation confirms that the role can read the current account ID. It also confirms that the encoded trust policy has a valid Terraform structure.
You should see confirmation that the configuration is valid. The rule role now has an account-scoped trust definition.
Trust policy validation failed?
- Check that the service principal is iot.amazonaws.com.
- Check that the condition references data.aws_caller_identity.current.account_id.
- Compare every bracket in the jsonencode value with the code above.
Still blocked? Help me debug the account-scoped AWS IoT trust policy in my Terraform configuration.
The role still needs an authorization policy for the rule action. The policy grants record writes only on the new table while preserving the endpoint discovery permission required by the Timestream action.
- Add the inline policy below the role resource in main.tf by copying this code:
resource "aws_iam_role_policy" "iot_timestream" {
name = "medical-iot-timestream-write"
role = aws_iam_role.iot_timestream.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["timestream:WriteRecords"]
Resource = aws_timestreamwrite_table.vitals.arn
},
{
Effect = "Allow"
Action = ["timestream:DescribeEndpoints"]
Resource = "*"
}
]
})
}
Why are the permissions separated?
- The timestream:WriteRecords permission targets only the ARN of the vitals table.
- The timestream:DescribeEndpoints permission uses * because endpoint discovery is not tied to one table ARN.
- The policy attaches to the role through aws_iam_role.iot_timestream.id.
- Save main.tf.
- Check the least-privilege policy by running:
terraform validate
What does this check prove?
A successful validation confirms that the policy can reference the role ID and table ARN. It also confirms that the two permission statements are structurally valid.
You should see confirmation that the configuration is valid. The role can now write to the intended table without receiving write access to every table.
Policy validation failed?
- Check that the role references aws_iam_role.iot_timestream.id.
- Check that the write permission references aws_timestreamwrite_table.vitals.arn.
- Confirm that each statement has its own matching braces.
Need help narrowing it down? Help me troubleshoot the least-privilege Timestream inline policy.
Route messages and deploy the pipeline
The final resource connects the MQTT topic filter to the Timestream table. It also derives a reusable device_id dimension from the third segment of each matching topic.
- Add the IoT topic rule below the inline policy in main.tf by copying this code:
resource "aws_iot_topic_rule" "medical_vitals" {
name = "medical_vitals_to_timestream"
description = "Route synthetic medical vital signs to Amazon Timestream"
enabled = true
sql = "SELECT heart_rate_bpm, spo2_percent, temperature_c FROM 'medical/vitals/+'"
sql_version = "2016-03-23"
timestream {
database_name = aws_timestreamwrite_database.medical_iot.database_name
table_name = aws_timestreamwrite_table.vitals.table_name
role_arn = aws_iam_role.iot_timestream.arn
dimension {
name = "device_id"
value = "$${topic(3)}"
}
}
depends_on = [aws_iam_role_policy.iot_timestream]
}
How does the routing rule work?
- The rule selects heart_rate_bpm, spo2_percent, plus temperature_c from messages matching medical/vitals/+.
- The + wildcard matches one topic level such as device-001.
- The escaped $${topic(3)} value reaches AWS IoT Core as ${topic(3)} and extracts the third topic segment.
- The explicit dependency ensures that the inline policy exists before Terraform creates the enabled rule.
- Save main.tf.
- Check the completed routing definition by running:
terraform validate
What does this check prove?
A successful validation confirms that the rule can reference the database, table, role, and policy. It also verifies the Terraform structure around the topic-derived dimension.
You should see confirmation that the configuration is valid. The full storage and routing graph is now defined.
Rule validation failed?
- Confirm that the SQL topic filter is medical/vitals/+.
- Confirm that the dimension value includes the Terraform escape in $${topic(3)}.
- Check that depends_on references aws_iam_role_policy.iot_timestream.
Still seeing a validation problem? Help me debug the AWS IoT Core topic rule and its Timestream action.
Terraform outputs provide a quick checkpoint after deployment. They expose the database, table, subscription filter, and concrete publishing topic without requiring a console search.
- Create outputs.tf in the Visual Studio Code Explorer sidebar with these outputs:
output "timestream_database" {
value = aws_timestreamwrite_database.medical_iot.database_name
}
output "timestream_table" {
value = aws_timestreamwrite_table.vitals.table_name
}
output "mqtt_topic_filter" {
value = "medical/vitals/+"
}
output "mqtt_topic" {
value = "medical/vitals/device-001"
}
What do these outputs expose?
- The storage outputs read the deployed database and table names from their resources.
- The MQTT outputs preserve the wildcard subscription filter and the concrete device publishing topic.
- Running terraform output after deployment displays all four values from Terraform state.
- Save outputs.tf.
- Check all four Terraform files by running:
terraform validate
What does the final validation prove?
This validation checks the complete configuration before Terraform contacts AWS to create resources. A successful result means the four files are syntactically valid and internally consistent.
You should see confirmation that the configuration is valid. That result clears the configuration for planning.
Final validation failed?
- Use the full-code reference below to compare each file name and resource reference.
- Confirm that all four files are saved in the medical-iot-telemetry folder.
- Confirm that the terminal is still using the same folder.
Need help reading the diagnostic? Help me trace my Terraform validation failure to the correct file.
✔️ Awesome, I've got everything!
Great work. Your four saved files now describe the persistent telemetry path and its deployment outputs.
ⓧ I'd like to double check the full code
Compare each saved file with the complete references below. Keep every resource and value in the same order.
terraform {
required_version = "= 1.16.2"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.64.0"
}
}
}
This file pins Terraform and the AWS provider to the project versions.
variable "aws_region" {
description = "AWS Region for the medical IoT telemetry lab"
type = string
default = "us-east-1"
}
This file sets the shared AWS region for the lab.
provider "aws" {
region = var.aws_region
}
data "aws_caller_identity" "current" {}
resource "aws_timestreamwrite_database" "medical_iot" {
database_name = "medical_iot"
}
resource "aws_timestreamwrite_table" "vitals" {
database_name = aws_timestreamwrite_database.medical_iot.database_name
table_name = "vitals"
retention_properties {
memory_store_retention_period_in_hours = 1
magnetic_store_retention_period_in_days = 1
}
}
resource "aws_iam_role" "iot_timestream" {
name = "medical-iot-timestream-rule-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "iot.amazonaws.com"
}
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}
]
})
}
resource "aws_iam_role_policy" "iot_timestream" {
name = "medical-iot-timestream-write"
role = aws_iam_role.iot_timestream.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["timestream:WriteRecords"]
Resource = aws_timestreamwrite_table.vitals.arn
},
{
Effect = "Allow"
Action = ["timestream:DescribeEndpoints"]
Resource = "*"
}
]
})
}
resource "aws_iot_topic_rule" "medical_vitals" {
name = "medical_vitals_to_timestream"
description = "Route synthetic medical vital signs to Amazon Timestream"
enabled = true
sql = "SELECT heart_rate_bpm, spo2_percent, temperature_c FROM 'medical/vitals/+'"
sql_version = "2016-03-23"
timestream {
database_name = aws_timestreamwrite_database.medical_iot.database_name
table_name = aws_timestreamwrite_table.vitals.table_name
role_arn = aws_iam_role.iot_timestream.arn
dimension {
name = "device_id"
value = "$${topic(3)}"
}
}
depends_on = [aws_iam_role_policy.iot_timestream]
}
This file defines the storage resources, IAM access, and MQTT routing rule.
output "timestream_database" {
value = aws_timestreamwrite_database.medical_iot.database_name
}
output "timestream_table" {
value = aws_timestreamwrite_table.vitals.table_name
}
output "mqtt_topic_filter" {
value = "medical/vitals/+"
}
output "mqtt_topic" {
value = "medical/vitals/device-001"
}
This file exposes the deployed storage names and MQTT topics.
A Terraform plan previews the dependency graph without creating anything. This is your chance to inspect the proposed resources before AWS starts billing for the deployed storage.
- Preview the infrastructure changes by running:
terraform plan
What should the plan contain?
- The plan should include the medical_iot database.
- The plan should include the vitals table.
- The plan should include the IAM role and its inline policy.
- The plan should include the enabled IoT topic rule.
You should see a plan proposing five managed resources. Terraform has not created them at this point.
Plan did not complete?
- Read the first Terraform diagnostic because later messages often follow from the same issue.
- Run terraform validate again if the diagnostic points to a configuration file.
- Confirm that your AWS CLI session is still authenticated if the diagnostic mentions access or credentials.
Need help interpreting the plan failure? Help me diagnose why Terraform cannot plan my medical IoT resources.
AWS charges start with the apply
Applying this plan creates billable Timestream resources. Timestream magnetic storage has a 100 GB per-account, per-Region billing minimum.
The final cleanup section destroys the lab after you finish querying the telemetry. Keeping the retention periods short also limits how long this synthetic history remains available.
Terraform pauses after presenting its plan and asks for approval. The resources are created only after you approve that prompt.
- Start the deployment by running:
terraform apply
What does apply do?
Terraform creates a fresh execution plan and requests approval because no saved plan was supplied. It executes the approved operations after you confirm the prompt.
- Review the proposed resource changes in the terminal.
- Enter yes at the approval prompt.
The deployment may pause while AWS creates the resources. When the command finishes successfully, the database, table, role, policy, and routing rule are managed in Terraform state.
Apply did not finish?
- Check the first AWS authorization message if the deployment cannot create a resource.
- Confirm that your sandbox role can manage AWS IoT Core, Timestream, IAM, and STS resources.
- Run terraform plan again after fixing the reported permission or configuration issue.
Need help with the failed resource? Help me diagnose my Terraform apply failure without exposing AWS credentials.
Before you run the final check, predict which database name, table name, topic filter, and publishing topic Terraform will print.
- Display the deployed output values by running:
terraform output
What does this command confirm?
Terraform reads the root-module outputs from state. This confirms the names and topics attached to the infrastructure you just deployed.
You should see medical_iot, vitals, medical/vitals/+, plus medical/vitals/device-001 in the output. That proves the persistent telemetry path is deployed with the expected destinations.
Outputs missing or unexpected?
- Confirm that terraform apply completed before checking the outputs.
- Compare outputs.tf with the full-code reference above.
- Confirm that the terminal path still ends with medical-iot-telemetry.
Still missing a value? Help me troubleshoot my Terraform outputs after a successful apply.
Your live-only MQTT test now has a durable destination backed by Terraform-managed infrastructure. Next, you will publish several readings and query their device-scoped history.
Route and Query Vital-Signs History
Your Terraform deployment now connects AWS IoT Core to Amazon Timestream for LiveAnalytics. The live MQTT message can finally become durable history.
This step tests the entire path with three synthetic readings. You will use a SQL query to prove that each reading became device-scoped measures with an ingestion time.
In this step, get ready to:
- Publish three synthetic vital-sign readings to medical/vitals/device-001.
- Build a recent-history query in queries.sql.
- Verify nine time-stamped rows for device-001.
Publish three synthetic readings
A JSON object represents one device reading. The topic rule turns each selected field into a separate Timestream measure.
- Return to the AWS Management Console from earlier.
- Confirm the Region menu displays US East (N. Virginia).
- Use the console search bar to find AWS IoT Core.
- Select AWS IoT Core from the search results.
- Select Test in the left navigation menu.
- Select MQTT test client.
- Select Publish to a topic.
- Enter medical/vitals/device-001 in Topic name.
- Copy one line at a time from this payload set:
{"heart_rate_bpm":72,"spo2_percent":98,"temperature_c":36.8}
{"heart_rate_bpm":76,"spo2_percent":97,"temperature_c":36.9}
{"heart_rate_bpm":74,"spo2_percent":98,"temperature_c":37.1}
How do these readings become measures?
- Each line is one complete MQTT message from the simulated device.
- Whole numbers preserve integer measures for heart rate and oxygen saturation.
- The decimal temperature preserves a double measure.
- The rule writes each selected field as its own Timestream record.
- Paste the first line into the message editor.
- Select Publish.
- Replace the message editor contents with the second line.
- Select Publish.
- Replace the message editor contents with the third line.
- Select Publish.
You have now sent three synthetic readings through the deployed topic rule.
Unable to publish a reading?
- Confirm Topic name contains medical/vitals/device-001 exactly.
- Confirm each message contains one complete JSON object from the payload set.
- Confirm the AWS Management Console still shows US East (N. Virginia).
Still stuck? Help me troubleshoot why my synthetic MQTT readings are not publishing..
Create and run the history query
The recent-history query reads the device dimension beside each measure. It also exposes the typed value columns that distinguish integral readings from decimal readings.
- In the Visual Studio Code file sidebar from earlier, select the new-file control.
- Name the file queries.sql.
- Fill queries.sql with the recent-history query by copying this code:
SELECT
time,
device_id,
measure_name,
measure_value::bigint AS integer_value,
measure_value::double AS decimal_value
FROM "medical_iot"."vitals"
WHERE time > ago(1h)
ORDER BY time DESC
LIMIT 20
What does this query do?
- The SELECT list retrieves the ingestion time, device dimension, measure name, and typed measure values.
- The measure_value::bigint expression reads integral measures into integer_value.
- The measure_value::double expression reads decimal measures into decimal_value.
- The time > ago(1h) condition keeps the result focused on the last hour.
- The ORDER BY time DESC clause places the newest records first.
- The LIMIT 20 clause caps the result at twenty rows.
- Save queries.sql.
✔️ Awesome, I've got everything!
Your queries.sql file is saved with the recent-history query.
ⓧ I'd like to double check the full code
- Compare your queries.sql file with this complete version:
SELECT
time,
device_id,
measure_name,
measure_value::bigint AS integer_value,
measure_value::double AS decimal_value
FROM "medical_iot"."vitals"
WHERE time > ago(1h)
ORDER BY time DESC
LIMIT 20
- Use the AWS Management Console search bar to find Amazon Timestream for LiveAnalytics.
- Select Amazon Timestream for LiveAnalytics from the search results.
- Select Query editor.
- Select medical_iot in the left pane.
- Select vitals in the left pane.
- Copy all text from queries.sql.
- Paste the copied query into the query editor.
Before you run the query, how many rows do you expect from three messages that each contain three selected fields?
- Run the recent-history query by selecting Run.
You should see nine recent rows for device-001. Each MQTT message produced three stored measures.
No rows in the result?
- Confirm the AWS Management Console still shows US East (N. Virginia).
- Return to the MQTT test client from earlier if the readings were not published successfully.
- Republish the three payloads from the previous substep if the one-hour query window has passed.
- Compare the query editor contents with the complete queries.sql reference.
Need another pair of eyes? Help me diagnose why my Timestream query returns no telemetry rows..
Inspect the stored measures
The IoT rule stores each selected payload attribute as its own record. The rule uses its processing time because the action does not supply a timestamp.
- Confirm the result contains nine rows labeled device-001.
- Confirm the heart_rate_bpm rows show 72, 76, and 74 in integer_value.
- Confirm the spo2_percent rows show 98, 97, and 98 in integer_value.
- Confirm the temperature_c rows show 36.8, 36.9, and 37.1 in decimal_value.
- Check the time column for an ingestion timestamp on every row.
Strong finish. Your synthetic readings now travel from one MQTT topic into durable device-scoped history that you can query.
Secret mission
Prove Multi-Device Isolation
Can one telemetry table keep readings from two simulated devices independently queryable? Publish a second device reading. Then prove that the topic-derived device dimension isolates its three measures.
Clean Up Your Resources
Clean Up Your Resources
Amazon Timestream for LiveAnalytics storage can keep incurring charges after this lab. Choose whether to keep the lab running, pause activity, or delete every resource.
Cost warning
Amazon Timestream for LiveAnalytics magnetic storage has a 100 GB per-account, per-Region billing minimum. AWS IoT Core also meters messaging plus rules engine evaluations and actions.
Deleting the lab promptly is the clearest way to stop these resources from generating new usage.
Resources you used:
- Terraform-managed Timestream database medical_iot plus table vitals with one-hour memory retention plus one-day magnetic retention.
- AWS IoT Core topic rule medical_vitals_to_timestream for messages matching medical/vitals/+.
- IAM role medical-iot-timestream-rule-role plus inline policy medical-iot-timestream-write.
- Local medical-iot-telemetry folder containing the Terraform configuration, state, provider cache, dependency lock file, queries.sql, plus queries-device-002.sql.
Keep everything running
No action is needed. Choose this option if you are continuing the telemetry lab or preparing to build on it.
- Leave the five Terraform-managed cloud resources deployed.
- Keep the medical-iot-telemetry folder available in Visual Studio Code.
- Monitor your AWS usage because Timestream storage charges can continue.
Pause - I'll come back to this later
Stop active messaging plus queries while keeping the deployed infrastructure available. Existing Timestream storage can still incur charges during this pause.
- Stop publishing synthetic messages to medical/vitals/device-001.
- Stop publishing synthetic messages to medical/vitals/device-002.
- Stop running queries against medical_iot.vitals.
- Keep the medical-iot-telemetry folder so Terraform can manage the deployed resources when you return.
Delete - I don't want to use this again
Removing the lab is permanent. Terraform gives you a preview before it changes anything, so you stay in control.
- Preview every cloud deletion by running this command:
terraform plan -destroy
What does this preview do?
Terraform builds a deletion plan without changing your resources. The preview lets you check that the database, table, topic rule, role, plus policy are included before deletion.
- Review the deletion preview.
- Confirm the preview proposes deleting five managed cloud resources.
- Delete the deployed cloud resources by running this command:
- Enter yes when Terraform asks for approval.
terraform destroy
What does this command do?
Terraform deprovisions every AWS object managed by this configuration. Your local configuration, state, provider cache, dependency lock file, plus query files remain available until you remove the folder.
- Confirm the terminal reports Destroy complete!.
That is the risky part finished. Your billable lab resources are now removed.
- Close Visual Studio Code.
- Use Windows File Explorer to navigate to the location containing medical-iot-telemetry.
- Delete the medical-iot-telemetry folder.
- Confirm the folder no longer appears in File Explorer.
Nice Work!
Nice Work!
You did it! You built a Terraform-managed medical IoT pipeline that routes synthetic MQTT readings through AWS IoT Core into Amazon Timestream for LiveAnalytics. Your stored heart-rate, oxygen-saturation, and temperature history is now queryable by device and time.
You've learned how to:
- Publish synthetic vital signs through the MQTT topic filter medical/vitals/+. The live subscription proved the ingestion edge works. Leaving the test client exposed the missing durable history.
- Provision a Terraform dependency graph for time-series storage. You also created least-privilege IAM access with an account-scoped trust condition. The enabled IoT topic rule sends matching measures directly to the medical_iot database.
- Query time-stamped device history with queries.sql. The results separate records by device_id. The typed value columns distinguish integer measures from decimal measures.
- Complete the optional Secret Mission by publishing readings from device-002. Your device-specific query returned three measures without any device-001 rows.
Ready to quiz yourself?