Provisions the AWS IoT Core device fleet β registry (Things/Types/Groups), security credentials (policies/certificates/authorizers/role aliases), message routing (topic rules), and fleet provisioning β as one aggregation module with no single keystone. Built for the AWS provider v6.x.
- π‘ Device registry.
aws_iot_thing,aws_iot_thing_type,aws_iot_thing_group,aws_iot_thing_group_membership, andaws_iot_billing_groupmodel the fleet and its hierarchy/cost allocation. - π Security credentials.
aws_iot_policy(+ attachment),aws_iot_certificate,aws_iot_ca_certificate,aws_iot_thing_principal_attachment,aws_iot_authorizer, andaws_iot_role_aliascover device auth end to end. - π¬ Message routing.
aws_iot_topic_rule(SQL + up to 11 action types) andaws_iot_topic_rule_destination(private VPC HTTP endpoints) route device telemetry to the rest of the estate. - π Fleet provisioning.
aws_iot_provisioning_template(JITP/bulk) andaws_iot_domain_configuration(custom data-plane domain + TLS policy). - βοΈ Account-wide settings.
aws_iot_logging_options,aws_iot_event_configurations, andaws_iot_indexing_configurationβ modeled as singletons, not maps, because there is only ever one per account/Region. - π§© No keystone. Things, Policies, Certificates, and Topic Rules are independent AWS resources with no inherent Terraform parent/child edge β every resource is named by role and rendered via
for_each, per this module suite's aggregation-module exception. - π¨ Rules never fail silently.
error_actionis a required, validated field on every topic rule β a rule with no error destination would otherwise drop failed device telemetry with zero observability.
π‘ Why it matters: device fleets are the widest, least-trusted edge of the network β a compromised or misconfigured certificate/policy/rule can touch production ingestion pipelines. This module makes the secure posture (signed authorizers, required error actions, no-private-key-by-default certificates) the path of least resistance.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
tf-mod-aws-iot-core sits downstream of the identity, compute, storage, and messaging foundations β it wires device credentials to IAM roles and routes telemetry into the app-integration/storage/database layer.
flowchart LR
IAMROLE["tf-mod-aws-iam-role"] --> IOTCORE["tf-mod-aws-iot-core"]
LAMBDA["tf-mod-aws-lambda"] --> IOTCORE
S3["tf-mod-aws-s3-bucket"] --> IOTCORE
SNS["tf-mod-aws-sns"] --> IOTCORE
SQS["tf-mod-aws-sqs"] --> IOTCORE
KINESIS["tf-mod-aws-kinesis-stream"] --> IOTCORE
FIREHOSE["tf-mod-aws-kinesis-firehose"] --> IOTCORE
DYNAMODB["tf-mod-aws-dynamodb"] --> IOTCORE
CWLOGS["tf-mod-aws-cloudwatch-log-group"] --> IOTCORE
VPC["tf-mod-aws-vpc"] --> IOTCORE
SG["tf-mod-aws-security-group"] --> IOTCORE
IOTCORE --> CWALARM["tf-mod-aws-cloudwatch-alarm"]
style IOTCORE fill:#FF9900,color:#fff
Rendered and syntax-validated via the Mermaid Chart MCP (
validate_and_render_mermaid_diagram) this session βvalid: true.
flowchart TB
subgraph Registry["Device registry"]
Thing["aws_iot_thing"]
ThingType["aws_iot_thing_type"]
ThingGroup["aws_iot_thing_group"]
Membership["aws_iot_thing_group_membership"]
BillingGroup["aws_iot_billing_group"]
end
subgraph Security["Security credentials"]
Policy["aws_iot_policy"]
PolicyAttach["aws_iot_policy_attachment"]
Cert["aws_iot_certificate"]
CACert["aws_iot_ca_certificate"]
PrincipalAttach["aws_iot_thing_principal_attachment"]
Authorizer["aws_iot_authorizer"]
RoleAlias["aws_iot_role_alias"]
end
subgraph Routing["Message routing"]
TopicRule["aws_iot_topic_rule"]
RuleDest["aws_iot_topic_rule_destination"]
end
subgraph Provisioning["Fleet provisioning"]
ProvTemplate["aws_iot_provisioning_template"]
DomainConfig["aws_iot_domain_configuration"]
end
subgraph Account["Account-wide singletons"]
LoggingOpts["aws_iot_logging_options"]
EventConfig["aws_iot_event_configurations"]
IndexConfig["aws_iot_indexing_configuration"]
end
Thing --> Membership
ThingGroup --> Membership
Thing --> PrincipalAttach
Cert --> PrincipalAttach
Policy --> PolicyAttach
Cert --> PolicyAttach
ThingType -.-> Thing
CACert -.-> ProvTemplate
Authorizer -.-> DomainConfig
TopicRule --> RuleDest
style Thing fill:#FF9900,color:#fff
style ThingType fill:#FF9900,color:#fff
style ThingGroup fill:#FF9900,color:#fff
style Membership fill:#FF9900,color:#fff
style BillingGroup fill:#FF9900,color:#fff
style Policy fill:#FF9900,color:#fff
style PolicyAttach fill:#FF9900,color:#fff
style Cert fill:#FF9900,color:#fff
style CACert fill:#FF9900,color:#fff
style PrincipalAttach fill:#FF9900,color:#fff
style Authorizer fill:#FF9900,color:#fff
style RoleAlias fill:#FF9900,color:#fff
style TopicRule fill:#FF9900,color:#fff
style RuleDest fill:#FF9900,color:#fff
style ProvTemplate fill:#FF9900,color:#fff
style DomainConfig fill:#FF9900,color:#fff
style LoggingOpts fill:#FF9900,color:#fff
style EventConfig fill:#FF9900,color:#fff
style IndexConfig fill:#FF9900,color:#fff
Rendered and syntax-validated via the Mermaid Chart MCP this session β
valid: true. Solid edges are in-module cross-references resolved by*_keyfields; dotted edges are optional cross-references.
| Resource | Count | Created when |
|---|---|---|
aws_iot_thing |
0..N | one per things entry |
aws_iot_thing_type |
0..N | one per thing_types entry |
aws_iot_thing_group |
0..N | one per thing_groups entry |
aws_iot_thing_group_membership |
0..N | one per thing_group_memberships entry |
aws_iot_thing_principal_attachment |
0..N | one per thing_principal_attachments entry |
aws_iot_billing_group |
0..N | one per billing_groups entry |
aws_iot_policy |
0..N | one per policies entry |
aws_iot_policy_attachment |
0..N | one per policy_attachments entry |
aws_iot_certificate |
0..N | one per certificates entry |
aws_iot_ca_certificate |
0..N | one per ca_certificates entry |
aws_iot_authorizer |
0..N | one per authorizers entry |
aws_iot_role_alias |
0..N | one per role_aliases entry |
aws_iot_topic_rule |
0..N | one per topic_rules entry |
aws_iot_topic_rule_destination |
0..N | one per topic_rule_destinations entry |
aws_iot_provisioning_template |
0..N | one per provisioning_templates entry |
aws_iot_domain_configuration |
0..N | one per domain_configurations entry |
aws_iot_logging_options |
0 or 1 | logging_options != null (account-wide singleton) |
aws_iot_event_configurations |
0 or 1 | event_configurations != null (account-wide singleton) |
aws_iot_indexing_configuration |
0 or 1 | indexing_configuration != null (account-wide singleton) |
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
The module declares only a required_providers block (providers.tf) and inherits the configured provider. No provider {} block, no credential variable, no region variable β IoT Core has no us-east-1 global-service constraint (custom domain configurations use ACM certs in their own Region).
Least-privilege actions the Terraform execution identity needs, grouped by resource. No service-linked role is auto-created by any in-scope IoT Core resource.
| Action | Required for | Notes |
|---|---|---|
iot:CreateThing, iot:DescribeThing, iot:UpdateThing, iot:DeleteThing |
things |
β |
iot:CreateThingType, iot:DescribeThingType, iot:DeprecateThingType, iot:DeleteThingType |
thing_types |
β |
iot:CreateThingGroup, iot:DescribeThingGroup, iot:UpdateThingGroup, iot:DeleteThingGroup |
thing_groups |
β |
iot:AddThingToThingGroup, iot:RemoveThingFromThingGroup |
thing_group_memberships |
β |
iot:AttachThingPrincipal, iot:DetachThingPrincipal |
thing_principal_attachments |
β |
iot:CreateBillingGroup, iot:DescribeBillingGroup, iot:UpdateBillingGroup, iot:DeleteBillingGroup |
billing_groups |
β |
iot:CreatePolicy, iot:GetPolicy, iot:CreatePolicyVersion, iot:DeletePolicyVersion, iot:DeletePolicy, iot:ListPolicyVersions |
policies |
Provider auto-prunes the oldest non-default version past 5 |
iot:AttachPolicy, iot:DetachPolicy |
policy_attachments |
β |
iot:CreateCertificateFromCsr, iot:CreateKeysAndCertificate, iot:RegisterCertificate, iot:RegisterCertificateWithoutCA, iot:DescribeCertificate, iot:UpdateCertificate, iot:DeleteCertificate |
certificates |
β |
iot:RegisterCACertificate, iot:DescribeCACertificate, iot:UpdateCACertificate, iot:DeleteCACertificate, iot:GetRegistrationCode |
ca_certificates |
Registration code fetch is a manual out-of-band step |
iot:CreateAuthorizer, iot:DescribeAuthorizer, iot:UpdateAuthorizer, iot:DeleteAuthorizer |
authorizers |
β |
iot:CreateRoleAlias, iot:DescribeRoleAlias, iot:UpdateRoleAlias, iot:DeleteRoleAlias, iam:PassRole (on the aliased role) |
role_aliases |
PassRole scoped to the specific role ARN |
iot:CreateTopicRule, iot:GetTopicRule, iot:ReplaceTopicRule, iot:DeleteTopicRule, iam:PassRole (on every action role_arn) |
topic_rules |
PassRole scoped per action role |
iot:CreateTopicRuleDestination, iot:GetTopicRuleDestination, iot:UpdateTopicRuleDestination, iot:DeleteTopicRuleDestination, iam:PassRole |
topic_rule_destinations |
β |
iot:CreateProvisioningTemplate, iot:DescribeProvisioningTemplate, iot:UpdateProvisioningTemplate, iot:DeleteProvisioningTemplate, iam:PassRole |
provisioning_templates |
β |
iot:CreateDomainConfiguration, iot:DescribeDomainConfiguration, iot:UpdateDomainConfiguration, iot:DeleteDomainConfiguration |
domain_configurations |
β |
iot:SetV2LoggingOptions, iot:GetV2LoggingOptions, iam:PassRole (on the logging role) |
logging_options (account-wide) |
Only one caller per account/Region should manage this |
iot:UpdateEventConfigurations, iot:DescribeEventConfigurations |
event_configurations (account-wide) |
Same singleton-ownership caveat |
iot:UpdateIndexingConfiguration, iot:GetIndexingConfiguration |
indexing_configuration (account-wide) |
Same singleton-ownership caveat |
iot:TagResource, iot:UntagResource, iot:ListTagsForResource |
Tagging | On every taggable resource type |
β οΈ iam:PassRoleis scoped, not blanket. Everyrole_arnin this module (topic-rule actions, role aliases, provisioning templates, logging options, topic rule destinations) is consumed by reference; the Terraform execution identity needsiam:PassRoleonly on those specific ARNs, granted at the caller's discretion β this module never creates or assumes IAM roles itself.
- Device certificates are metadata/lifecycle registration, not key custody. Prefer supplying
csrso no private key ever transits Terraform state; letting the module generate a keypair is a lab/dev-only pattern. logging_options,event_configurations, andindexing_configurationare ACCOUNT-WIDE SINGLETONS per Region. Only one module call per account/Region should manage each β a second caller fights the first over the same account-level setting. Document ownership in your root-module composition.- No service-linked role required for any in-scope IoT Core resource.
- IoT endpoint is account/Region-specific β devices connect to
iot:<random>.iot.<region>.amazonaws.com(or a custom domain viadomain_configurations). This module does not create the endpoint (aws_iot_endpointis a data source, out of scope); wire it via a data source in the root module. - Certificate/policy/thing quotas are soft account limits (e.g. 1M things, 10K thing types, 100K policies per account by default) β expect a quota-increase request for large fleets.
- Custom domain configurations require ACM certificates provisioned in the resource's own Region β IoT Core data-plane domains are regional, unlike CloudFront. No us-east-1 requirement.
- Registering a CA certificate requires a registration code (from the
aws_iot_registration_codedata source, out of scope) burned into a verification CSR whencertificate_mode = "DEFAULT"β a manual out-of-band step beforeca_certificate_pem/verification_certificate_pemcan be supplied.
tf-mod-aws-iot-core/
βββ providers.tf # required_providers only β no provider {} block
βββ variables.tf # 19 role-named input maps + tags tail
βββ main.tf # 19 for_each resource blocks, no `this` keystone
βββ outputs.tf # role-named id/arn maps + tags_all
βββ README.md
βββ SCOPE.md
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
things = {
"line1-sensor-001" = {}
}
policies = {
"device-default" = {
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["iot:Connect", "iot:Publish", "iot:Subscribe", "iot:Receive"]
Resource = "*"
}]
})
}
}
certificates = {
"line1-sensor-001-cert" = {
active = true
csr = file("${path.module}/device-csrs/line1-sensor-001.csr")
}
}
thing_principal_attachments = {
"line1-sensor-001-attach" = {
thing_key = "line1-sensor-001"
certificate_key = "line1-sensor-001-cert"
}
}
policy_attachments = {
"line1-sensor-001-policy" = {
policy_key = "device-default"
target_certificate_key = "line1-sensor-001-cert"
}
}
topic_rules = {
"telemetry-to-lambda" = {
enabled = true
sql = "SELECT * FROM 'factory/line1/telemetry'"
actions = {
forward = {
lambda = { function_arn = module.telemetry_processor.arn } # tf-mod-aws-lambda
}
}
error_action = {
sqs = {
queue_url = module.telemetry_dlq.queue_url # tf-mod-aws-sqs
role_arn = module.iot_rule_role.arn # tf-mod-aws-iam-role
use_base64 = false
}
}
}
}
tags = {
environment = "prod"
workload = "factory-telemetry"
}
}| Input | Type | Source module |
|---|---|---|
authorizers[*].authorizer_function_arn |
string (Lambda ARN) |
tf-mod-aws-lambda |
topic_rules[*].actions[*].lambda.function_arn |
string (Lambda ARN) |
tf-mod-aws-lambda |
topic_rules[*].actions[*].s3.bucket_name |
string |
tf-mod-aws-s3-bucket |
topic_rules[*].actions[*].sns.target_arn |
string (SNS ARN) |
tf-mod-aws-sns |
topic_rules[*].actions[*].sqs.queue_url |
string (SQS URL) |
tf-mod-aws-sqs |
topic_rules[*].actions[*].kinesis.stream_name |
string |
tf-mod-aws-kinesis-stream |
topic_rules[*].actions[*].firehose.delivery_stream_name |
string |
tf-mod-aws-kinesis-firehose |
topic_rules[*].actions[*].dynamodb.table_name |
string |
tf-mod-aws-dynamodb |
every role_arn argument |
string (IAM role ARN) |
tf-mod-aws-iam-role |
topic_rule_destinations[*].vpc_configuration.subnet_ids / vpc_id |
list(string) / string |
tf-mod-aws-vpc |
topic_rule_destinations[*].vpc_configuration.security_groups |
list(string) |
tf-mod-aws-security-group |
| This module output | Feeds into |
|---|---|
thing_ids / thing_arns |
thing_group_memberships, thing_principal_attachments (in-module), device provisioning tooling |
thing_default_client_ids |
Device SDK bootstrap config |
thing_type_ids / thing_type_arns |
things[*].thing_type_key wiring, audit |
thing_group_ids / thing_group_arns |
thing_group_memberships (in-module), IAM policy conditions |
thing_group_membership_ids |
Audit |
thing_principal_attachment_ids |
Audit |
billing_group_ids / billing_group_arns |
Cost allocation tooling |
policy_arns / policy_names / policy_default_version_ids |
policy_attachments (in-module), audit |
policy_attachment_ids |
Audit |
certificate_ids / certificate_arns / certificate_pems |
thing_principal_attachments, policy_attachments (in-module) |
certificate_private_keys (sensitive) |
Emergency device bootstrap only |
ca_certificate_ids / ca_certificate_arns |
Device manufacturing/CA tooling |
authorizer_arns |
domain_configurations.default_authorizer_key wiring |
role_alias_arns |
Device SDK configuration |
topic_rule_arns |
tf-mod-aws-cloudwatch-alarm, audit |
topic_rule_destination_arns |
topic_rules[*].actions[*].http wiring |
provisioning_template_arns / provisioning_template_default_version_ids |
Fleet-provisioning device SDK config |
domain_configuration_arns / domain_configuration_domain_types |
DNS/CNAME wiring |
logging_options_configured / event_configurations_configured / indexing_configuration_configured |
Ownership audit for the account-wide singletons |
tags_all |
Governance/audit |
1 Β· Minimal device registration (thing + policy + certificate)
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
things = {
"sensor-001" = {}
}
policies = {
"sensor-default" = {
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["iot:Connect", "iot:Publish", "iot:Subscribe", "iot:Receive"]
Resource = "*"
}]
})
}
}
certificates = {
"sensor-001-cert" = {
active = true
csr = file("${path.module}/csrs/sensor-001.csr")
}
}
thing_principal_attachments = {
"sensor-001-attach" = {
thing_key = "sensor-001"
certificate_key = "sensor-001-cert"
}
}
policy_attachments = {
"sensor-001-policy" = {
policy_key = "sensor-default"
target_certificate_key = "sensor-001-cert"
}
}
}2 Β· Thing types, groups, and hierarchy
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
thing_types = {
"temperature-sensor" = {
properties = {
description = "Line temperature sensors"
searchable_attributes = ["line", "zone"]
}
}
}
thing_groups = {
"factory-line1" = {
properties = { description = "Line 1 devices" }
}
}
things = {
"sensor-001" = {
attributes = { line = "1", zone = "A" }
thing_type_key = "temperature-sensor"
}
}
thing_group_memberships = {
"sensor-001-in-line1" = {
thing_key = "sensor-001"
thing_group_key = "factory-line1"
}
}
}3 Β· Topic rule routing telemetry to a Lambda
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
topic_rules = {
"telemetry-to-lambda" = {
enabled = true
sql = "SELECT *, timestamp AS ingest_ts FROM 'factory/+/telemetry'"
sql_version = "2016-03-23"
actions = {
process = {
lambda = { function_arn = module.telemetry_lambda.arn } # tf-mod-aws-lambda
}
}
error_action = {
cloudwatch_metric = {
metric_namespace = "IoTCore/RuleErrors"
metric_name = "telemetry-to-lambda"
metric_value = "1"
metric_unit = "Count"
role_arn = module.iot_rule_role.arn # tf-mod-aws-iam-role
}
}
}
}
}4 Β· Topic rule routing to S3 with error_action to an SQS DLQ
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
topic_rules = {
"telemetry-to-s3" = {
enabled = true
sql = "SELECT * FROM 'factory/+/telemetry'"
actions = {
archive = {
s3 = {
bucket_name = module.telemetry_bucket.id # tf-mod-aws-s3-bucket
key = "$${topic}/$${timestamp}.json"
role_arn = module.iot_rule_role.arn # tf-mod-aws-iam-role
}
}
}
error_action = {
sqs = {
queue_url = module.telemetry_dlq.queue_url # tf-mod-aws-sqs
role_arn = module.iot_rule_role.arn
use_base64 = false
}
}
}
}
}5 Β· Fleet provisioning template (JITP)
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
provisioning_templates = {
"fleet-jitp" = {
description = "Just-in-time provisioning for line sensors"
enabled = true
provisioning_role_arn = module.provisioning_role.arn # tf-mod-aws-iam-role
template_type = "FLEET_PROVISIONING"
template_body = jsonencode({
Parameters = {
SerialNumber = { Type = "String" }
}
Resources = {
thing = {
Type = "AWS::IoT::Thing"
Properties = {
ThingName = { Ref = "SerialNumber" }
}
}
}
})
pre_provisioning_hook = {
target_arn = module.provisioning_hook_lambda.arn # tf-mod-aws-lambda
}
}
}
}6 Β· Custom Lambda-backed authorizer
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
authorizers = {
"custom-jwt-auth" = {
authorizer_function_arn = module.authorizer_lambda.arn # tf-mod-aws-lambda
signing_disabled = false
status = "ACTIVE"
token_key_name = "x-amz-custom-authorization-token"
token_signing_public_keys = {
"key1" = file("${path.module}/keys/authorizer-public-key-1.pem")
}
}
}
}7 Β· Least-privilege policy scoped by thing name / client ID
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
policies = {
"scoped-device-policy" = {
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "iot:Connect"
Resource = "arn:aws:iot:*:*:client/$${iot:Connection.Thing.ThingName}"
},
{
Effect = "Allow"
Action = ["iot:Publish", "iot:Receive"]
Resource = "arn:aws:iot:*:*:topic/factory/$${iot:Connection.Thing.ThingName}/*"
},
{
Effect = "Allow"
Action = "iot:Subscribe"
Resource = "arn:aws:iot:*:*:topicfilter/factory/$${iot:Connection.Thing.ThingName}/*"
}
]
})
}
}
}8 Β· tags merging with provider default_tags
# Root provider block (not part of this module):
# provider "aws" {
# default_tags {
# tags = { managed-by = "terraform", cost-center = "iot-platform" }
# }
# }
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
policies = {
"device-default" = {
policy = jsonencode({ Version = "2012-10-17", Statement = [] })
tags = { component = "policy" } # per-item tag, merges over module tags
}
}
tags = {
environment = "prod"
workload = "factory-telemetry"
}
}
# Resulting tags_all["policy::device-default"] contains managed-by, cost-center
# (from provider default_tags), environment, workload (module tags), and
# component (per-item tag) β resource-level tags win on any key conflict.9 Β· Certificate lifecycle β CSR flow (no private key in state)
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
certificates = {
"sensor-042-cert" = {
active = true
csr = file("${path.module}/csrs/sensor-042.csr") # AWS signs; no private key ever generated by Terraform
}
}
}
# certificate_private_keys["sensor-042-cert"] will be an empty string β the
# module never generated a keypair for this entry.10 Β· Secure-by-default opt-out β signing-disabled authorizer behind a private mTLS boundary
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
authorizers = {
"vpc-internal-auth" = {
authorizer_function_arn = module.internal_authorizer_lambda.arn
# Opt-out of signature validation β ONLY acceptable because this
# authorizer sits behind a private VPC endpoint with mutual TLS already
# authenticating the caller. Document the compensating control here.
signing_disabled = true
status = "ACTIVE"
}
}
}11 Β· for_each pattern β a fleet of things from a list
locals {
sensor_ids = ["sensor-001", "sensor-002", "sensor-003"]
}
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
things = {
for id in local.sensor_ids : id => {
attributes = { fleet = "line1" }
}
}
certificates = {
for id in local.sensor_ids : "${id}-cert" => {
active = true
csr = file("${path.module}/csrs/${id}.csr")
}
}
thing_principal_attachments = {
for id in local.sensor_ids : "${id}-attach" => {
thing_key = id
certificate_key = "${id}-cert"
}
}
}12 Β· Import an existing Thing and Policy
import {
to = module.iot_core.aws_iot_thing.this["sensor-001"]
id = "sensor-001"
}
import {
to = module.iot_core.aws_iot_policy.this["device-default"]
id = "device-default"
}13 Β· Account-wide logging + indexing (own this exactly once per account/Region)
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
logging_options = {
default_log_level = "ERROR"
disable_all_logs = false
role_arn = module.iot_logging_role.arn # tf-mod-aws-iam-role
}
indexing_configuration = {
thing_indexing_configuration = {
thing_indexing_mode = "REGISTRY_AND_SHADOW"
}
thing_group_indexing_configuration = {
thing_group_indexing_mode = "ON"
}
}
}14 Β· Private VPC HTTP topic-rule destination
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
topic_rule_destinations = {
"internal-api" = {
vpc_configuration = {
role_arn = module.iot_destination_role.arn # tf-mod-aws-iam-role
subnet_ids = module.vpc.private_subnet_ids # tf-mod-aws-vpc
security_groups = [module.iot_endpoint_sg.id] # tf-mod-aws-security-group
vpc_id = module.vpc.id
}
}
}
}15 Β· End-to-end composition β device fleet routing telemetry to Lambda, S3, and a DLQ
module "iot_rule_role" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
assume_role_policy = data.aws_iam_policy_document.iot_trust.json
inline_policies = {
"iot-rule-actions" = { policy = data.aws_iam_policy_document.iot_rule_actions.json }
}
}
module "telemetry_bucket" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-s3-bucket?ref=v1.0.0"
bucket = "casey-factory-telemetry"
}
module "telemetry_lambda" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lambda?ref=v1.0.0"
function_name = "telemetry-processor"
#... runtime/handler/role wiring omitted for brevity
}
module "telemetry_dlq" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-sqs?ref=v1.0.0"
name = "iot-telemetry-dlq"
}
module "iot_core" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iot-core?ref=v1.0.0"
thing_types = {
"line-sensor" = { properties = { description = "Factory line sensors" } }
}
thing_groups = {
"factory-line1" = { properties = { description = "Line 1 device fleet" } }
}
things = {
for id in ["sensor-001", "sensor-002"] : id => {
thing_type_key = "line-sensor"
}
}
thing_group_memberships = {
for id in ["sensor-001", "sensor-002"] : "${id}-membership" => {
thing_key = id
thing_group_key = "factory-line1"
}
}
policies = {
"device-default" = {
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["iot:Connect", "iot:Publish", "iot:Subscribe", "iot:Receive"]
Resource = "arn:aws:iot:*:*:*/$${iot:Connection.Thing.ThingName}/*"
}]
})
}
}
certificates = {
for id in ["sensor-001", "sensor-002"] : "${id}-cert" => {
active = true
csr = file("${path.module}/csrs/${id}.csr")
}
}
thing_principal_attachments = {
for id in ["sensor-001", "sensor-002"] : "${id}-attach" => {
thing_key = id
certificate_key = "${id}-cert"
}
}
policy_attachments = {
for id in ["sensor-001", "sensor-002"] : "${id}-policy" => {
policy_key = "device-default"
target_certificate_key = "${id}-cert"
}
}
topic_rules = {
"telemetry-fanout" = {
enabled = true
sql = "SELECT * FROM 'factory/line1/+/telemetry'"
actions = {
process = { lambda = { function_arn = module.telemetry_lambda.arn } }
archive = {
s3 = {
bucket_name = module.telemetry_bucket.id
key = "$${topic}/$${timestamp}.json"
role_arn = module.iot_rule_role.arn
}
}
}
error_action = {
sqs = {
queue_url = module.telemetry_dlq.queue_url
role_arn = module.iot_rule_role.arn
use_base64 = false
}
}
}
}
tags = {
environment = "prod"
workload = "factory-telemetry"
}
}Registry: things, thing_types, thing_groups, thing_group_memberships, thing_principal_attachments, billing_groups
Security: policies, policy_attachments, certificates, ca_certificates, authorizers, role_aliases
Routing & provisioning: topic_rules, topic_rule_destinations, provisioning_templates, domain_configurations
Account-wide singletons: logging_options, event_configurations, indexing_configuration
Universal tail: tags (map(string), default {})
No separate global
timeoutsvariable β of the 19 in-scope resources, onlyaws_iot_topic_rule_destinationsupports atimeoutsblock, socreate/update/deleteis modeled as a per-item field ontopic_rule_destinationsrather than a module-wide input.
thing_ids,thing_arns,thing_default_client_idsthing_type_ids,thing_type_arnsthing_group_ids,thing_group_arnsthing_group_membership_ids,thing_principal_attachment_idsbilling_group_ids,billing_group_arnspolicy_arns,policy_names,policy_default_version_ids,policy_attachment_idscertificate_ids,certificate_arns,certificate_pemscertificate_private_keysβ sensitive; empty string except for entries where the module generated a keypairca_certificate_ids,ca_certificate_arnsauthorizer_arns,role_alias_arnstopic_rule_arns,topic_rule_destination_arnsprovisioning_template_arns,provisioning_template_default_version_idsdomain_configuration_arns,domain_configuration_domain_typeslogging_options_configured,event_configurations_configured,indexing_configuration_configuredβ booleans flagging whether this module call owns each account-wide singletontags_allβ map of"<resource_type>::<key>"=> merged tags; untaggable resource types are not represented
- ARN formats:
arn:aws:iot:<region>:<account>:thing/<name>,.../thinggroup/<name>,.../policy/<name>,.../cert/<id>,.../authorizer/<name>,.../rolealias/<name>,.../rule/<name>,.../provisioningtemplate/<name>. - Immutable / force-new:
things[*].name(the map key),thing_types[*]name andproperties.description/properties.searchable_attributes,thing_groups[*].name, everycertificates[*]field (certificates cannot be updated in place β active toggling is the one mutable field), and every resource's map key in general since names double as Terraform keys. tagsβtags_allβ providerdefault_tags: taggable resources (thing_types,thing_groups,billing_groups,policies,ca_certificates,authorizers,role_aliases,topic_rules,provisioning_templates,domain_configurations) merge moduletags, per-item tags, and providerdefault_tagsβ resource-level tags win on key conflict.things,thing_group_memberships,thing_principal_attachments,policy_attachments,certificates,topic_rule_destinations, and the three account-wide singletons are not taggable in the AWS provider.- Eventual consistency: newly attached certificates/policies can take a few seconds to propagate to the MQTT broker; a device connecting immediately after
applymay see a transient auth failure. - Destroy ordering:
aws_iot_policy_attachment/aws_iot_thing_principal_attachmentmust be destroyed before the certificate or policy they reference; using this module's*_keycross-references (rather than literal ARNs) lets Terraform's implicit dependency graph handle this automatically. - No us-east-1 global-resource requirement β IoT Core (including custom domain configurations) is fully regional.
- Intra-module self-reference limitation:
thing_groups[*].parent_group_namecannot be resolved from a sibling map key (that would create a Terraform dependency cycle onaws_iot_thing_group.thisreferencing itself) β supply a literal parent name and apply parents before children if nesting groups created by this same module call.
- No keystone. Things, Policies, Certificates, and Topic Rules are independent AWS resources with no inherent parent/child Terraform relationship β every resource is named by role and exposed as its own
for_eachmap, per this module suite's aggregation-module exception. error_actionis required, not optional. Opt-out: there is none by design β every topic rule must configure exactly one error destination (point it at a CloudWatch Logs group or SQS DLQ).sql_versiondefaults to"2016-03-23"(current GA). Opt-out: set"2015-10-08"or"beta"explicitly.authorizers.signing_disableddefaults tofalse(token signature validated). Opt-out:true, only for authorizers behind a private VPC/mTLS boundary that already authenticates the caller.role_aliases.credential_durationdefaults to3600(1h). Opt-out: raise up to43200(12h) only when a workload genuinely needs it.domain_configurations.tls_config.security_policydefaults toIoTSecurityPolicy_TLS1_2_2018_06(strongest available). Opt-out: relax only for documented legacy-device compatibility.ca_certificates.active/allow_auto_registrationhave no default β both are required fields so a caller must consciously enable device auth and auto-registration for a CA.- Certificate private-key emission is empty by default. Opt-out: omit both
csrandcertificate_pemto let the module generate a keypair (lab/dev only) βcertificate_private_keysthen surfaces the key, markedsensitive. - Topic-rule action surface is intentionally scoped to 11 of ~18 possible action types (s3, sns, sqs, lambda, dynamodb, cloudwatch_metric, cloudwatch_alarm, kinesis, firehose, republish, http). Extend the shared action
objecttype invariables.tfand the matchingdynamicblocks inmain.tfforcloudwatch_logs,dynamodbv2,elasticsearch,iot_analytics,iot_events,kafka,step_functions, ortimestreamwhen a caller needs one. - Account-wide singletons are single optional objects, not maps β there can only ever be one
logging_options/event_configurations/indexing_configurationper account/Region.
cd tf-mod-aws-iot-core
terraform init -backend=false
terraform validate
terraform fmt -check
β οΈ terraform plan/applyrequire valid AWS credentials (profile / SSO / OIDC) and a Region. Pin the module source with?ref=v1.0.0β never a branch β so a re-plan never silently picks up unreviewed changes.
terraform plan
terraform apply
terraform outputterraform init -backend=false && terraform validate && terraform fmt -checkβ the offline structural gate; both pass with zero errors/diffs as of this authoring session.tflint(AWS ruleset) andcheckovvia the sharedvalidate_all_modules.ps1harness for static security/policy checks.- No
terraform applyis performed by this authoring process β an accountable human runsapplyin a non-production account with a least-privilege role.
$ terraform apply
Plan: 7 to add, 0 to change, 0 to destroy.
Outputs:
thing_arns = {
"sensor-001" = "arn:aws:iot:us-east-2:123456789012:thing/sensor-001"
}
policy_arns = {
"device-default" = "arn:aws:iot:us-east-2:123456789012:policy/device-default"
}
certificate_arns = {
"sensor-001-cert" = "arn:aws:iot:us-east-2:123456789012:cert/ab12cd34ef56..."
}
- Tag drift from
default_tagsoverlap β a key set both in the provider'sdefault_tagsand in this module'stags(or a per-itemtags) will show perpetual plan diffs if the values differ; resource-level tags always win. Keep the same key in only one place, or make the values match. - Credential-chain failures at
plan/applyβterraform validatenever touches AWS, butplanneeds a resolvable credential chain (AWS_PROFILE, SSO session, or OIDC role). Runaws sts get-caller-identityto confirm the chain resolves before troubleshooting the module itself. AccessDeniedoniot:CreateTopicRule/iot:CreateRoleAliasβ check theiam:PassRolegrant on the specificrole_arnused; this module never widens IAM permissions itself.- Device connects but gets disconnected immediately β usually a
certificates.active = falsetoggle that just propagated, or apolicy_attachmentsentry missing for that certificate. Confirm both the principal attachment and the policy attachment exist for the certificate the device presents. aws_iot_thing_groupapply fails with "group not found" for a nested parent β this module cannot express an intra-module dependency forparent_group_namereferencing a sibling created in the same call (see Architecture Notes); apply the parent group first, or reference an externally-managed parent.- Second
applyfrom a different root module keeps flippinglogging_options/event_configurations/indexing_configurationβ these are account-wide singletons with no delete API; two module calls managing the same one will fight forever. Assign single ownership. aws_iot_ca_certificateapply fails demandingverification_certificate_pemβ required whenevercertificate_mode = "DEFAULT"(the default); obtain the registration code via theaws_iot_registration_codedata source and build the verification CSR out of band first.
- Terraform Registry: AWS IoT Core resources (
aws_iot_thing,aws_iot_policy,aws_iot_certificate,aws_iot_topic_rule, and siblings) - AWS IoT Core Developer Guide β Device provisioning, Rules Engine, IoT SQL reference
- AWS IoT Core security best practices β X.509 certificate lifecycle, custom authorizers
SCOPE.md(this module) β in/out-of-scope resources, Required IAM permissions, AWS Prerequisites
π§‘ "Infrastructure as Code should be standardized, consistent, and secure."