|
| 1 | +--- |
| 2 | +canonical_url: https://dev.to/networkandcode/setup-iot-core-on-google-cloud-with-terraform-4h6a |
| 3 | +cover_image: https://source.unsplash.com/featured/?iot |
| 4 | +tags: cloud, googlecloud, iot, terraform |
| 5 | +--- |
| 6 | + |
| 7 | +Hello :wave:, we shall see how to provision a minimal IoT infrastructure on Google cloud with Terraform. |
| 8 | + |
| 9 | +I shall be doing this straight on the Google cloud shell... |
| 10 | + |
| 11 | +## Project |
| 12 | +Set your gcloud config... |
| 13 | + |
| 14 | +Get you projects list and set one of the projects as the current project. |
| 15 | +``` |
| 16 | +$ gcloud projects list |
| 17 | + |
| 18 | +$ gcloud config set project <project-id> |
| 19 | +``` |
| 20 | + |
| 21 | +## Directories |
| 22 | +Let's create two directories for the terraform resources, one for the service account and another for rest of the resources. |
| 23 | +``` |
| 24 | +$ mkdir ~/sa |
| 25 | +$ mkdir ~/iot |
| 26 | +``` |
| 27 | +and one more hidden directory for storing the keys/certificates. |
| 28 | +``` |
| 29 | +$ mkdir ~/.auth |
| 30 | +``` |
| 31 | + |
| 32 | +## TF Provider |
| 33 | +We would set the Terraform provider configuration here. |
| 34 | + |
| 35 | +Get the list of zones in the specific region. Note that cloud IoT is currently supported in these regions: asia-east1, europe-west1, us-central1. |
| 36 | +``` |
| 37 | +$ gcloud compute zones list --filter="region~asia-east1" | grep -i name |
| 38 | +NAME: asia-east1-b |
| 39 | +NAME: asia-east1-a |
| 40 | +NAME: asia-east1-c |
| 41 | +``` |
| 42 | + |
| 43 | +I would be using zone c. |
| 44 | + |
| 45 | +Set the provider details in terraform with the available information. |
| 46 | +``` |
| 47 | +$ cat ~/sa/main.tf |
| 48 | +provider "google" { |
| 49 | + project = "<project-id>" |
| 50 | + region = "asia-east1" |
| 51 | + zone = "asia-east1-c" |
| 52 | +} |
| 53 | + |
| 54 | +$ cp ~/sa/main.tf ~/iot/main.tf |
| 55 | +``` |
| 56 | + |
| 57 | +## Service account |
| 58 | +We are going to create a service account from our user account, which could be further used for creating other resources using terraform. |
| 59 | +``` |
| 60 | +$ ls ~/sa |
| 61 | +main.tf outputs.tf sa.tf |
| 62 | + |
| 63 | +$ cat ~/sa/sa.tf |
| 64 | +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_service_account |
| 65 | + |
| 66 | +resource "google_service_account" "iot_sa" { |
| 67 | + account_id = "iot-sa" |
| 68 | + display_name = "IoT Service Account" |
| 69 | +} |
| 70 | + |
| 71 | +# note this requires the terraform to be run regularly |
| 72 | +resource "time_rotating" "iot_sa_key_rotation" { |
| 73 | + rotation_days = 30 |
| 74 | +} |
| 75 | + |
| 76 | +resource "google_service_account_key" "iot_sa_key" { |
| 77 | + service_account_id = google_service_account.iot_sa.name |
| 78 | + |
| 79 | + keepers = { |
| 80 | + rotation_time = time_rotating.iot_sa_key_rotation.rotation_rfc3339 |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +resource "google_project_iam_member" "iot_editor" { |
| 85 | + project = var.project_id |
| 86 | + role = "roles/cloudiot.editor" |
| 87 | + member = "serviceAccount:${google_service_account.iot_sa.email}" |
| 88 | + |
| 89 | + condition { |
| 90 | + title = "expires_after_2022_07_31" |
| 91 | + description = "Expiring at midnight of 2022-07-31" |
| 92 | + expression = "request.time < timestamp(\"2022-08-01T00:00:00Z\")" |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +resource "google_project_iam_member" "pub_sub_editor" { |
| 97 | + project = var.project_id |
| 98 | + role = "roles/pubsub.editor" |
| 99 | + member = "serviceAccount:${google_service_account.iot_sa.email}" |
| 100 | + |
| 101 | + condition { |
| 102 | + title = "expires_after_2022_07_31" |
| 103 | + description = "Expiring at midnight of 2022-07-31" |
| 104 | + expression = "request.time < timestamp(\"2022-08-01T00:00:00Z\")" |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +$ cat ~/sa/variables.tf |
| 109 | +variable "project_id" { |
| 110 | + type = string |
| 111 | + default = "<project-id>" |
| 112 | +} |
| 113 | + |
| 114 | +$ cat ~/sa/outputs.tf |
| 115 | +output "iot_sa_private_key" { |
| 116 | + description = "Private key of the IoT service account" |
| 117 | + value = google_service_account_key.iot_sa_key.private_key |
| 118 | + sensitive = true |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +So we are creating a service account with editor roles on IoT core & Pub/Sub, a key for the service account with rotation, and then we would output the private key to save it locally for future use. |
| 123 | + |
| 124 | +## API |
| 125 | +We have to enable the Cloud IoT [API](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_service), you can get the fqdn of it using `$ gcloud services list --available --filter="name~.*iot.*"`. Let's add the terraform configuration which can enable it. |
| 126 | +``` |
| 127 | +$ cat ~/sa/variables.tf |
| 128 | +variable "project_id" { |
| 129 | + type = string |
| 130 | + default = "<project-id>" |
| 131 | +} |
| 132 | + |
| 133 | +$ cat ~/sa/apis.tf |
| 134 | +resource "google_project_service" "cloudiot" { |
| 135 | + project = var.project_id |
| 136 | + service = "cloudiot.googleapis.com" |
| 137 | + |
| 138 | + timeouts { |
| 139 | + create = "30m" |
| 140 | + update = "40m" |
| 141 | + } |
| 142 | + |
| 143 | + disable_dependent_services = true |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +## Apply |
| 148 | +We can now create the service account, it's associated resources, and enable the Cloud IoT API. |
| 149 | +``` |
| 150 | +$ cd ~/sa |
| 151 | +$ terraform init |
| 152 | + |
| 153 | +# optional, to know what will be changed |
| 154 | +$ terraform plan |
| 155 | + |
| 156 | +$ terraform apply --auto-approve |
| 157 | +``` |
| 158 | + |
| 159 | +## Validate |
| 160 | +Validate the service account creation, via the console. |
| 161 | + |
| 162 | + |
| 163 | +And the roles attached to it. |
| 164 | + |
| 165 | + |
| 166 | +## Key |
| 167 | +The private key of the service account could be retrieved from the terraform output. |
| 168 | +``` |
| 169 | +$ terraform output -raw iot_sa_private_key | base64 -d > ~/.auth/iot_sa_private_key.json |
| 170 | +``` |
| 171 | +We have saved the base64 decoded private key in a hidden auth directory at home. |
| 172 | + |
| 173 | +## Credentials |
| 174 | +We could now start using the service principal's private key as a credential for rest of our Terraform activities, for which we have to set an environment variable. |
| 175 | +``` |
| 176 | +$ export GOOGLE_APPLICATION_CREDENTIALS=~/.auth/iot_sa_private_key.json |
| 177 | +``` |
| 178 | + |
| 179 | +Note: to remove the credential anytime, jus run `unset GOOGLE_APPLICATION_CREDENTIALS` |
| 180 | + |
| 181 | +## Certificate |
| 182 | +The connection between the IoT devices and Google IoT core would be secure over TLS, hence a [certificate](https://cloud.google.com/iot/docs/create-device-registry#create_your_credentials) should be generated for our virtual device. |
| 183 | +``` |
| 184 | +$ openssl req -x509 -newkey rsa:2048 -keyout ~/.auth/rsa_private.pem -nodes -out ~/.auth/rsa_cert.pem -subj "/CN=unused" |
| 185 | + |
| 186 | +$ ls ~/.auth/ | grep pem |
| 187 | +rsa_cert.pem |
| 188 | +rsa_private.pem |
| 189 | +``` |
| 190 | +The private key is in rsa_private.pem and the public certificate is in rsa_cert.pem. |
| 191 | + |
| 192 | +We would keep the private key locally and refer to it while generating a client connection from our device(we are not dealing with the client side of things in this post though), where as the public certificate would be attached to the remote side, in this case, the IoT core. |
| 193 | + |
| 194 | +## Registry |
| 195 | +Add the [terrafaorm](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloudiot_registry) configuration for the device registry, pub/sub topics it would use. |
| 196 | +``` |
| 197 | +$ cd ~/iot |
| 198 | + |
| 199 | +$ cat registry.tf |
| 200 | +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloudiot_registry |
| 201 | + |
| 202 | +resource "google_cloudiot_registry" "iot-registry" { |
| 203 | + name = "iot-registry" |
| 204 | + |
| 205 | + event_notification_configs { |
| 206 | + pubsub_topic_name = google_pubsub_topic.additional-telemetry.id |
| 207 | + subfolder_matches = "test/path" |
| 208 | + } |
| 209 | + |
| 210 | + event_notification_configs { |
| 211 | + pubsub_topic_name = google_pubsub_topic.default-telemetry.id |
| 212 | + subfolder_matches = "" |
| 213 | + } |
| 214 | + |
| 215 | + state_notification_config = { |
| 216 | + pubsub_topic_name = google_pubsub_topic.default-devicestatus.id |
| 217 | + } |
| 218 | + |
| 219 | + mqtt_config = { |
| 220 | + mqtt_enabled_state = "MQTT_ENABLED" |
| 221 | + } |
| 222 | + |
| 223 | + http_config = { |
| 224 | + http_enabled_state = "HTTP_ENABLED" |
| 225 | + } |
| 226 | + |
| 227 | + log_level = "INFO" |
| 228 | +} |
| 229 | +``` |
| 230 | +We would be using 3 topics, all messages published by the client to the path /devices/DEVICE_ID/events would go to the default telemetry topic, and all messages for /devices/DEVICE_ID/state would go to the default device state topic. We have one additional topic with sub folder path "test/path" which means the messages published to /devices/DEVICE_ID/events/test/path would land there. |
| 231 | + |
| 232 | +## Pub/Sub |
| 233 | +A separate file for creating the pub/sub topics which will be linked to the registry. |
| 234 | +``` |
| 235 | +resource "google_pubsub_topic" "default-devicestatus" { |
| 236 | + name = "default-devicestatus" |
| 237 | +} |
| 238 | + |
| 239 | +resource "google_pubsub_topic" "default-telemetry" { |
| 240 | + name = "default-telemetry" |
| 241 | +} |
| 242 | + |
| 243 | +resource "google_pubsub_topic" "additional-telemetry" { |
| 244 | + name = "additional-telemetry" |
| 245 | +} |
| 246 | +``` |
| 247 | +## Devices |
| 248 | +We would be creating two devices, a basic one which should bind with the gateway, and an advanced device that could be standalone with out a gateway. |
| 249 | + |
| 250 | +The authentication for the basic device will be handled by the gateway and hence, we don't have to set any credentials for the basic device. |
| 251 | +``` |
| 252 | +$ cat basic-device.tf |
| 253 | +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloudiot_device |
| 254 | + |
| 255 | +resource "google_cloudiot_device" "basic-device" { |
| 256 | + name = "basic-device" |
| 257 | + registry = google_cloudiot_registry.iot-registry.id |
| 258 | +} |
| 259 | + |
| 260 | +$ cat advanced-device.tf |
| 261 | +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloudiot_device |
| 262 | + |
| 263 | +resource "google_cloudiot_device" "advanced-device" { |
| 264 | + name = "advanced-device" |
| 265 | + registry = google_cloudiot_registry.iot-registry.id |
| 266 | + |
| 267 | + credentials { |
| 268 | + public_key { |
| 269 | + format = "RSA_X509_PEM" |
| 270 | + key = file("~/.auth/rsa_cert.pem") |
| 271 | + } |
| 272 | + } |
| 273 | +} |
| 274 | +``` |
| 275 | + |
| 276 | +## Gateway |
| 277 | +And now the gateway. |
| 278 | +``` |
| 279 | +# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloudiot_device |
| 280 | + |
| 281 | +resource "google_cloudiot_device" "iot-gateway" { |
| 282 | + name = "iot-gateway" |
| 283 | + registry = google_cloudiot_registry.iot-registry.id |
| 284 | + |
| 285 | + credentials { |
| 286 | + public_key { |
| 287 | + format = "RSA_X509_PEM" |
| 288 | + key = file("~/.auth/rsa_cert.pem") |
| 289 | + } |
| 290 | + } |
| 291 | + |
| 292 | + gateway_config { |
| 293 | + gateway_type = "GATEWAY" |
| 294 | + gateway_auth_method = "ASSOCIATION_ONLY" |
| 295 | + } |
| 296 | +} |
| 297 | +``` |
| 298 | +I have setup ASSOCIATION_ONLY as the auth method, which means the device I will bind to this gateway would rely on this gateway for authentication and woudln't authenticate with its own credential. |
| 299 | + |
| 300 | +## Apply |
| 301 | +The resources can be created. |
| 302 | +``` |
| 303 | +$ terraform init |
| 304 | + |
| 305 | +# optional, to see what will change |
| 306 | +$ terraform plan |
| 307 | + |
| 308 | +$ terraform apply |
| 309 | +``` |
| 310 | + |
| 311 | +## Bind |
| 312 | +The basic device should be bounded with the gateway, so that the gateway generate JWTs on behalf of the device. |
| 313 | +``` |
| 314 | +$ gcloud iot devices gateways bind --gateway iot-gateway --gateway-region asia-east1 --gateway-registry iot-registry --device basic-device --device-region asia-east1 |
| 315 | +``` |
| 316 | +I used gcloud for binding the device with the gateway as I was not able to quite find it in the terraform registry. |
| 317 | + |
| 318 | +## Validate |
| 319 | +Finally, check the resources on the console. |
| 320 | + |
| 321 | +*Registry* |
| 322 | + |
| 323 | + |
| 324 | +*Devices* |
| 325 | + |
| 326 | + |
| 327 | +*Gateway* |
| 328 | + |
| 329 | + |
| 330 | +*Device binding* |
| 331 | + |
| 332 | + |
| 333 | +Seems all good :) |
| 334 | + |
| 335 | +## Graph |
| 336 | +Let's look at the graph that terraform can generate. We can view it on the cloud shell editor itself. |
| 337 | + |
| 338 | +``` |
| 339 | +$ terraform graph | dot -Tsvg > graph.svg |
| 340 | + |
| 341 | +$ ls *.svg |
| 342 | +graph.svg |
| 343 | +``` |
| 344 | + |
| 345 | + |
| 346 | + |
| 347 | +With this the post is complete, thanks for reading !!! |
0 commit comments