AWS IoT Fleet Provisioning 用の Terraform モジュールを作ってみた

AWS IoT Fleet Provisioning 用の Terraform モジュールを作ってみた

2026.04.30

こんにちは!コンサルティング部のくろすけです!

今回は、AWS IoT Core の Fleet Provisioning を扱うための Terraform モジュールを作ってみたので、その内容をまとめます。

概要

今回作成したモジュールでは、主に以下をまとめて管理できるようにしています。

  • Fleet Provisioning by claim 用の Provisioning Template
  • クレーム証明書にアタッチする IoT Policy
  • プロビジョニング時に利用する IAM Role
  • プロビジョニング後にデバイスへ付与する IoT Policy
  • Lambda action を持つ IoT Rule

クレーム証明書自体は Terraform の外で事前作成し、その ARN をモジュールへ渡す形にしています。
Terraform で作成してしまうと、証明書の情報が state ファイルに保存されてしまうため、証明書漏洩のリスクを回避するためには外部で作成する方が良いと考えました。

Terraform モジュール

ディレクトリ構成概要

terraform-aws-iot/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── modules/
│   ├── provisioning/
│   │   └── fleet_claim/
│   │       ├── main.tf
│   │       ├── variables.tf
│   │       └── outputs.tf
│   └── rules/
│       └── lambda/
│           ├── main.tf
│           ├── variables.tf
│           └── outputs.tf
└── examples/
    └── basic/
        ├── main.tf
        └── templates/
            └── fleet_provisioning.json

ルートモジュールで全体の入力と組み立てを行い、modules/provisioning/fleet_claim で Fleet Provisioning 関連、modules/rules/lambda で IoT Rule 関連を分ける構成にしています。

主要なファイルのみ下記にて紹介します。

terraform-aws-iot/main.tf
# Fleet Provisioning by claim(クレーム証明書はモジュール外で事前作成)
module "provisioning_fleet_claim" {
  count  = var.provisioning.fleet_claim != null ? 1 : 0
  source = "./modules/provisioning/fleet_claim"

  name                         = var.name
  claim_certificate_arn        = var.provisioning.fleet_claim.claim_certificate_arn
  claim_certificate_policy_doc = var.provisioning.fleet_claim.claim_certificate_policy_doc
  claim_policy_name            = var.provisioning.fleet_claim.claim_policy_name
  template_body_path           = var.provisioning.fleet_claim.template_body_path
  fleet_provisioning_role_name = var.provisioning.fleet_claim.fleet_provisioning_role_name
  template_name                = var.provisioning.fleet_claim.template_name
  template_description         = var.provisioning.fleet_claim.template_description
  provisioning_role_arn        = var.provisioning.fleet_claim.provisioning_role_arn
  tags                         = var.tags
}

# Fleet Provisioning 後にデバイス証明書へ付与する IoT Policy
resource "aws_iot_policy" "device" {
  name   = var.device_policy.name
  policy = var.device_policy.doc

  tags = var.tags
}

# センサデータ種別ごとの IoT Rule with Lambda action
module "rules_lambda" {
  for_each = var.rules.lambda
  source   = "./modules/rules/lambda"

  name                = each.key
  description         = each.value.description
  sql                 = each.value.sql
  sql_version         = each.value.sql_version
  lambda_function_arn = each.value.lambda_function_arn
  iam_role_name       = each.value.iam_role_name
  invoke_lambda_policy_name = each.value.invoke_lambda_policy_name
  tags                = var.tags
}
terraform-aws-iot/variables.tf
variable "name" {
  description = "モジュール内リソース名のプレフィックス(IoT Policy 名・IAM ロール名・テンプレート名に使用)"
  type        = string
}

variable "provisioning" {
  description = "デバイスのプロビジョニング方式の設定"
  type = object({
    fleet_claim = optional(object({
      claim_certificate_arn        = string
      claim_certificate_policy_doc = string
      claim_policy_name            = optional(string, null)
      template_body_path           = string
      fleet_provisioning_role_name = optional(string, null)
      template_name                = optional(string, null)
      template_description         = optional(string, null)
      provisioning_role_arn        = optional(string, null)
    }), null)
    fleet_trusted_user = optional(object({
      template_body_path    = string
      provisioning_role_arn = optional(string, null)
    }), null)
  })
  default = {
    fleet_claim        = null
    fleet_trusted_user = null
  }

  validation {
    condition     = var.provisioning.fleet_trusted_user == null
    error_message = "provisioning.fleet_trusted_user は将来拡張用の予約済みフィールドで、まだ実装されていません。"
  }
}

variable "device_policy" {
  description = "Fleet Provisioning 後のデバイス証明書へ付与する IoT Policy の設定"
  type = object({
    name = optional(string, "device-policy")
    doc  = string
  })
}

variable "rules" {
  description = "IoT Rule の設定。現在は Lambda action のみサポート"
  type = object({
    lambda = optional(map(object({
      description         = optional(string, "")
      sql                 = string
      sql_version         = optional(string, "2016-03-23")
      lambda_function_arn = string
      iam_role_name       = optional(string, null)
      invoke_lambda_policy_name = optional(string, null)
    })), {})
    sqs = optional(map(object({
      description = optional(string, "")
      sql         = string
      sql_version = optional(string, "2016-03-23")
      queue_url   = string
    })), {})
  })
  default = {
    lambda = {}
    sqs    = {}
  }

  validation {
    condition     = length(var.rules.sqs) == 0
    error_message = "rules.sqs は将来拡張用の予約済みフィールドで、まだ実装されていません。"
  }
}

variable "tags" {
  description = "各リソースに付与するタグのマップ"
  type        = map(string)
  default     = {}
}
terraform-aws-iot/modules/provisioning/fleet_claim/main.tf
# クレーム証明書に付与する IoT Policy(Provisioning 用の限定権限)
locals {
  claim_policy_name          = var.claim_policy_name != null ? var.claim_policy_name : "${var.name}-claim-policy"
  fleet_provisioning_role_name = var.fleet_provisioning_role_name != null ? var.fleet_provisioning_role_name : "${var.name}-fleet-provisioning-role"
  template_name              = var.template_name != null ? var.template_name : "${var.name}-fleet-provisioning"
  template_description       = var.template_description != null ? var.template_description : "Fleet Provisioning template for ${var.name}"
}

resource "aws_iot_policy" "claim" {
  name   = local.claim_policy_name
  policy = var.claim_certificate_policy_doc

  tags = var.tags
}

# 事前作成済みのクレーム証明書へポリシーをアタッチ
resource "aws_iot_policy_attachment" "claim" {
  policy = aws_iot_policy.claim.name
  target = var.claim_certificate_arn
}

# Fleet Provisioning 用 IAM ロール(provisioning_role_arn 未指定時に作成)
data "aws_iam_policy_document" "iot_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["iot.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "fleet_provisioning" {
  count = var.provisioning_role_arn == null ? 1 : 0

  name               = local.fleet_provisioning_role_name
  assume_role_policy = data.aws_iam_policy_document.iot_assume_role.json
  tags               = var.tags
}

resource "aws_iam_role_policy_attachment" "fleet_provisioning_registration" {
  count = var.provisioning_role_arn == null ? 1 : 0

  role       = aws_iam_role.fleet_provisioning[0].name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSIoTThingsRegistration"
}

locals {
  provisioning_role_arn = var.provisioning_role_arn != null ? var.provisioning_role_arn : aws_iam_role.fleet_provisioning[0].arn
}

# Fleet Provisioning テンプレート
resource "aws_iot_provisioning_template" "this" {
  name                  = local.template_name
  description           = local.template_description
  provisioning_role_arn = local.provisioning_role_arn
  enabled               = true
  template_body         = file(var.template_body_path)

  tags = var.tags
}
terraform-aws-iot/modules/provisioning/fleet_claim/variables.tf
variable "name" {
  description = "リソース名のプレフィックス(IoT Policy 名・IAM ロール名・テンプレート名に使用)"
  type        = string
}

variable "claim_certificate_arn" {
  description = "事前作成済みのクレーム証明書 ARN"
  type        = string
}

variable "claim_certificate_policy_doc" {
  description = "クレーム証明書に付与する IoT Policy のドキュメント(JSON 文字列)。Provisioning 用の限定権限を定義する"
  type        = string
}

variable "claim_policy_name" {
  description = "クレーム証明書に付与する IoT Policy 名。null の場合は `<name>-claim-policy` を使用"
  type        = string
  default     = null
}

variable "template_body_path" {
  description = "Fleet Provisioning テンプレートの JSON ファイルパス"
  type        = string
}

variable "fleet_provisioning_role_name" {
  description = "Fleet Provisioning 用 IAM ロール名。null の場合は `<name>-fleet-provisioning-role` を使用"
  type        = string
  default     = null
}

variable "template_name" {
  description = "Fleet Provisioning テンプレート名。null の場合は `<name>-fleet-provisioning` を使用"
  type        = string
  default     = null
}

variable "template_description" {
  description = "Fleet Provisioning テンプレートの説明。null の場合は既定値を使用"
  type        = string
  default     = null
}

variable "provisioning_role_arn" {
  description = "Fleet Provisioning に使用する IAM ロールの ARN。null の場合はモジュール内で IAM ロールを作成する"
  type        = string
  default     = null
}

variable "tags" {
  description = "各リソースに付与するタグのマップ"
  type        = map(string)
  default     = {}
}
terraform-aws-iot/modules/rules/lambda/main.tf
data "aws_iam_policy_document" "iot_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["iot.amazonaws.com"]
    }
  }
}

locals {
  iam_role_name             = var.iam_role_name != null ? var.iam_role_name : "${var.name}-topic-rule-role"
  invoke_lambda_policy_name = var.invoke_lambda_policy_name != null ? var.invoke_lambda_policy_name : "${var.name}-invoke-lambda"
}

# IoT Topic Rule の Lambda action 用 IAM ロール
resource "aws_iam_role" "topic_rule" {
  name               = local.iam_role_name
  assume_role_policy = data.aws_iam_policy_document.iot_assume_role.json
  tags               = var.tags
}

data "aws_iam_policy_document" "invoke_lambda" {
  statement {
    actions   = ["lambda:InvokeFunction"]
    resources = [var.lambda_function_arn]
  }
}

resource "aws_iam_role_policy" "invoke_lambda" {
  name   = local.invoke_lambda_policy_name
  role   = aws_iam_role.topic_rule.id
  policy = data.aws_iam_policy_document.invoke_lambda.json
}

# IoT Topic Rule with Lambda action
resource "aws_iot_topic_rule" "this" {
  name        = var.name
  description = var.description
  enabled     = true
  sql         = var.sql
  sql_version = var.sql_version

  lambda {
    function_arn = var.lambda_function_arn
  }

  tags = var.tags
}

# IoT が Lambda を呼び出すための Lambda リソースベースポリシー
resource "aws_lambda_permission" "iot_invoke" {
  statement_id  = "AllowIoTInvoke-${var.name}"
  action        = "lambda:InvokeFunction"
  function_name = var.lambda_function_arn
  principal     = "iot.amazonaws.com"
  source_arn    = aws_iot_topic_rule.this.arn
}
terraform-aws-iot/modules/rules/lambda/variables.tf
variable "name" {
  description = "Lambda action を持つ Topic Rule の名前。英数字とアンダースコアのみ使用可能"
  type        = string

  validation {
    condition     = can(regex("^[a-zA-Z0-9_]+$", var.name))
    error_message = "name は英数字とアンダースコアのみ使用できます。"
  }
}

variable "description" {
  description = "Lambda action を持つ Topic Rule の説明"
  type        = string
  default     = ""
}

variable "sql" {
  description = "Topic Rule の SQL ステートメント(例: SELECT * FROM 'sensor/data1')"
  type        = string
}

variable "sql_version" {
  description = "IoT SQL エンジンのバージョン"
  type        = string
  default     = "2016-03-23"
}

variable "lambda_function_arn" {
  description = "Lambda action の送信先となる関数 ARN"
  type        = string
}

variable "iam_role_name" {
  description = "IoT Rule の Lambda action 用 IAM ロール名。null の場合は `<rule_name>-topic-rule-role` を使用"
  type        = string
  default     = null
}

variable "invoke_lambda_policy_name" {
  description = "IoT Rule の Lambda action 用インラインポリシー名。null の場合は `<rule_name>-invoke-lambda` を使用"
  type        = string
  default     = null
}

variable "tags" {
  description = "各リソースに付与するタグのマップ"
  type        = map(string)
  default     = {}
}

やってみた

1. クレーム証明書の準備

クレーム証明書は Terraform 管理外としているため、マネジメントコンソールから作成します。
証明書の各種ファイルはダウンロードしておき、証明書の ARN を控えておきましょう。

2. Terraform テンプレート

下記が、今回 Terraform モジュールを呼び出すテンプレートです。

terraform-aws-iot/examples/basic/main.tf
module "iot" {
  source = "../../"

  name = "smpl"

  device_policy = {
    name = "smpl-device-policy"
    doc = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Effect = "Allow"
          Action = [
            "iot:Connect",
            "iot:Publish",
            "iot:Subscribe",
            "iot:Receive",
          ]
          Resource = [
            "*"
          ]
        }
      ]
    })
  }

  provisioning = {
    fleet_claim = {
      # クレーム証明書は CLI などで事前作成し、ARN をこのモジュールへ渡す
      claim_certificate_arn = "arn:aws:iot:ap-northeast-1:123456789012:cert/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

      claim_policy_name = "smpl-claim-policy"
      # クレーム証明書に付与する IoT Policy(Provisioning 用の限定権限)
      claim_certificate_policy_doc = jsonencode({
        Version = "2012-10-17"
        Statement = [
          {
            Effect = "Allow"
            Action = [
              "iot:Connect",
              "iot:Publish",
              "iot:Subscribe",
              "iot:Receive",
            ]
            Resource = [
              "*"
            ]
          }
        ]
      })

      # Fleet Provisioning テンプレート
      template_body_path           = "${path.module}/templates/fleet_provisioning.json"
      fleet_provisioning_role_name = "smpl-fleet-provisioning-role"
      template_name                = "smpl-fleet-provisioning"
      template_description         = "Fleet Provisioning template for smpl devices"
      provisioning_role_arn        = null # null の場合はモジュール内で IAM ロールを作成
    }
  }

  # IoT Rule
  rules = {
    lambda = {
      sensor1 = {
        description               = "sensor1 -> lambda"
        sql                       = "SELECT * FROM 'dt/sensor1'"
        lambda_function_arn       = "arn:aws:lambda:ap-northeast-1:123456789012:function:sensor1:latest"
        iam_role_name             = "sensor1-topic-rule-role"
        invoke_lambda_policy_name = "sensor1-invoke-lambda"
      }
    }
  }

  tags = {
    Project     = "smpl"
    Environment = "dev"
  }
}
terraform-aws-iot/examples/basic/templates/fleet_provisioning.json
{
  "Parameters": {
    "SerialNumber": {
      "Type": "String"
    },
    "AWS::IoT::Certificate::Id": {
      "Type": "String"
    }
  },
  "Resources": {
    "certificate": {
      "Properties": {
        "CertificateId": {
          "Ref": "AWS::IoT::Certificate::Id"
        },
        "Status": "Active"
      },
      "Type": "AWS::IoT::Certificate"
    },
    "policy": {
      "Properties": {
        "PolicyName": "smpl-device-policy"
      },
      "Type": "AWS::IoT::Policy"
    },
    "thing": {
      "OverrideSettings": {
        "AttributePayload": "MERGE",
        "ThingGroups": "DO_NOTHING",
        "ThingTypeName": "REPLACE"
      },
      "Properties": {
        "AttributePayload": {
          "serialNumber": {
            "Ref": "SerialNumber"
          }
        },
        "ThingName": {
          "Fn::Join": [
            "",
            [
              "device-",
              {
                "Ref": "SerialNumber"
              }
            ]
          ]
        }
      },
      "Type": "AWS::IoT::Thing"
    }
  }
}

3. Terraform 実行

実行結果(作成されたリソース)になります。
※Lambda そのものはこのモジュールの管理外であり、サンプルの Lambda を呼び出しています。

プロビジョニングテンプレート

CleanShot20260430at20.49.37.png

クレームおよびデバイスポリシー

CleanShot20260430at20.50.49.png

IoT Rule(Lambda action)

CleanShot20260430at20.51.50.png
CleanShot20260430at20.52.11.png

4. テスト

モノの登録とメッセージの送信を行い、Lambda が呼び出されることを確認します。
※テストには下記で作成したスクリプトを使用しています。

https://dev.classmethod.jp/articles/try-aws-iot-fleet-provisioning-auto/

devices.json には下記を使用しています。

devices.json
{
  "endpoint": "XXXXXXXX-ats.iot.ap-northeast-1.amazonaws.com",
  "claim_cert": "./claim-cert.pem",
  "claim_key": "./claim-private.key",
  "root_ca": "./root-ca.pem",
  "template_name": "smpl-fleet-provisioning",
  "output_dir": "./certs",
  "verbosity": "NoLogs",
  "devices": [
    {
      "client_id": "sensor1",
      "parameters": {
        "SerialNumber": "sensor1"
      },
      "topic": "dt/sensor1"
    }
  ]
}

モノの登録

CleanShot20260430at20.58.20.png

メッセージ送信

CleanShot20260430at21.00.58.png

Lambda の呼び出し

Lambda のメトリクスを確認すると、Lambda が呼び出されたことが確認できました!
(Lambda 側はエラーとなっていますが、今回のテストではテキトーな Lambda を使用したのでご容赦ください。)

CleanShot20260430at21.02.37.png

あとがき

今回、IoT Core の Fleet Provisioning 向けの Terraform モジュールを作成してみました。

モジュールを公開したいと思いつつ、まだ特定の機能しか作成しておらず、完成度も低いのでもう少し機能を追加してから公開できればと思います。

以上、くろすけでした!

この記事をシェアする

関連記事