AWS IoT Fleet Provisioning 用の Terraform モジュールを作ってみた
こんにちは!コンサルティング部のくろすけです!
今回は、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 を呼び出しています。
プロビジョニングテンプレート

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

IoT Rule(Lambda action)


4. テスト
モノの登録とメッセージの送信を行い、Lambda が呼び出されることを確認します。
※テストには下記で作成したスクリプトを使用しています。
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"
}
]
}
モノの登録

メッセージ送信

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

あとがき
今回、IoT Core の Fleet Provisioning 向けの Terraform モジュールを作成してみました。
モジュールを公開したいと思いつつ、まだ特定の機能しか作成しておらず、完成度も低いのでもう少し機能を追加してから公開できればと思います。
以上、くろすけでした!




