Deploying Weather Dashboard Project using Nodered, Influx as well as Grafana via Terraform

Paul Zhao
21 min readMar 26, 2021
Project Infrastructure

What is Terraform and why we use it?

Terraform is an open-source infrastructure as code software tool created by HashiCorp. Users define and provide data center infrastructure using a declarative configuration language known as HashiCorp Configuration Language, or optionally JSON

Terraform, the leading force in IaC, allows to provision infrastructures in a seamless manner.

Advantages of Terraform:

  • Prevents configuration drift: Terraform binds you to make sure changes are firstly made in your container before it deploys the new ones across every server. By doing so, it separates server configuration from any dependency, which may cause identical instances across infrastructures
  • Easy collaboration: The terraform registry (Terraform’s central registry version control) enables teams to collaborate on infrastructure.
  • No separate documentation needed: The code written for infrastructure will become your documentation.
  • Flexibility: Terraform not only handles IaaS such as AWS, Azure, GCP but also PaaS such as SQL, NodeJS. It can also store local variables such as cloud tokens and passwords in encrypted form on the terraform registry.
  • Masterless: Terraform directly uses the cloud providers’ API, thus saving extra infrastructure costs and other overheads.

What is Grafana?

Grafana has become the world’s most popular technology used to compose observability dashboards with everything from Prometheus & Graphite metrics, to logs and application data to power plants and beehives.

In this project, Grafana would allow us to monitor change of temperature in Space center in House and Kennedy Space center in Florida.

What is Nodered?

Node-RED is a programming tool for wiring together hardware devices, APIs and online services in new and interesting ways.

It provides a browser-based editor that makes it easy to wire together flows using the wide range of nodes in the palette that can be deployed to its runtime in a single-click.

In this project, we use Nodered to map our infrastructures and make them work as shown below

Node-red workflow

What is Influx DB?

InfluxDB is the essential time series toolkit — dashboards, queries, tasks and agents all in one place.

In this project, we take advantage of it to accomplish our data management

In this project, we will exploring how we may use Terraform to seamlessly build up a weather app using Nodered, Influx as well as Grafana

We’ll be using AWS Cloud9 to provision our EC2 and host our application.

File structure

You can find out this project resource from here

Here I will not be explaining these files one after anther. But the broader concept of each file will be touch upon

First of all, from higher level, we have both container and image directories as modules to refer to. Both them include main.tf — the main terraform file, outputs.tf — intended outputs, providers.tf — provider’s info and variables.tf — store variables in main.tf file. Under container directory, we also have a volume directory to store our docker volumes, which also include main.tf, outputs.tf, providers.tf as well as variables.tf files

Secondly, under weatherapp directory, apart from these 4 files we introduced. We also have file for deployments for our three applications — locals.tf. terraform.tfvars file, where we stored ex_port info for our three different applications for both dev and prod environment. Then we include test.txt, in which we stored our applications ipv4 and port

Creating a non-root user

Based on AWS best practice, root user is not recommended to perform everyday tasks, even the administrative ones. The root user, rather is used to to create your first IAM user, groups and roles. Then you need to securely lock away the root user credentials and use them to perform only a few account and service management tasks.

Notes: If you would like to learn more about why we should not use root user for operations and more about AWS account, please find more here.

Login as a Root user
Create a user under IAM service
Choose programmatic access
Choose programmatic access
Create user without tags
Keep credentials (Access key ID and Secret access key)

Navigate to Cloud9 in AWS

Cloud9

Name environment

Name environment

Configure settings, and leave default options unless you would like to use your local environment to ssh into EC2 instance

Configure settings

Create Environment

Create environment

For a few minutes, Cloud9 will be provisioned

Cloud9 interface

For this project code to be working, you must create and change into prod workspace

$ terraform workspace new prod
Created and switched to workspace "prod"!
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

To apply terraform plan to double confirm resources that that would be created

$ terraform planAn execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:# module.container["grafana"].docker_container.app_container[0] will be created
+ resource "docker_container" "app_container" {
+ attach = false
+ bridge = (known after apply)
+ command = (known after apply)
+ container_logs = (known after apply)
+ entrypoint = (known after apply)
+ env = (known after apply)
+ exit_code = (known after apply)
+ gateway = (known after apply)
+ hostname = (known after apply)
+ id = (known after apply)
+ image = (known after apply)
+ init = (known after apply)
+ ip_address = (known after apply)
+ ip_prefix_length = (known after apply)
+ ipc_mode = (known after apply)
+ log_driver = "json-file"
+ logs = false
+ must_run = true
+ name = (known after apply)
+ network_data = (known after apply)
+ read_only = false
+ remove_volumes = true
+ restart = "no"
+ rm = false
+ security_opts = (known after apply)
+ shm_size = (known after apply)
+ start = true
+ stdin_open = false
+ tty = false
+ healthcheck {
+ interval = (known after apply)
+ retries = (known after apply)
+ start_period = (known after apply)
+ test = (known after apply)
+ timeout = (known after apply)
}
+ labels {
+ label = (known after apply)
+ value = (known after apply)
}
+ ports {
+ external = 3000
+ internal = 3000
+ ip = "0.0.0.0"
+ protocol = "tcp"
}
+ volumes {
+ container_path = "/etc/grafana"
+ volume_name = (known after apply)
}
+ volumes {
+ container_path = "/var/lib/grafana"
+ volume_name = (known after apply)
}
}
# module.container["grafana"].random_string.random[0] will be created
+ resource "random_string" "random" {
+ id = (known after apply)
+ length = 4
+ lower = true
+ min_lower = 0
+ min_numeric = 0
+ min_special = 0
+ min_upper = 0
+ number = true
+ result = (known after apply)
+ special = false
+ upper = false
}
# module.container["influxdb"].docker_container.app_container[0] will be created
+ resource "docker_container" "app_container" {
+ attach = false
+ bridge = (known after apply)
+ command = (known after apply)
+ container_logs = (known after apply)
+ entrypoint = (known after apply)
+ env = (known after apply)
+ exit_code = (known after apply)
+ gateway = (known after apply)
+ hostname = (known after apply)
+ id = (known after apply)
+ image = (known after apply)
+ init = (known after apply)
+ ip_address = (known after apply)
+ ip_prefix_length = (known after apply)
+ ipc_mode = (known after apply)
+ log_driver = "json-file"
+ logs = false
+ must_run = true
+ name = (known after apply)
+ network_data = (known after apply)
+ read_only = false
+ remove_volumes = true
+ restart = "no"
+ rm = false
+ security_opts = (known after apply)
+ shm_size = (known after apply)
+ start = true
+ stdin_open = false
+ tty = false
+ healthcheck {
+ interval = (known after apply)
+ retries = (known after apply)
+ start_period = (known after apply)
+ test = (known after apply)
+ timeout = (known after apply)
}
+ labels {
+ label = (known after apply)
+ value = (known after apply)
}
+ ports {
+ external = 8086
+ internal = 8086
+ ip = "0.0.0.0"
+ protocol = "tcp"
}
+ volumes {
+ container_path = "/var/lib/influxdb"
+ volume_name = (known after apply)
}
}
# module.container["influxdb"].random_string.random[0] will be created
+ resource "random_string" "random" {
+ id = (known after apply)
+ length = 4
+ lower = true
+ min_lower = 0
+ min_numeric = 0
+ min_special = 0
+ min_upper = 0
+ number = true
+ result = (known after apply)
+ special = false
+ upper = false
}
# module.container["nodered"].docker_container.app_container[0] will be created
+ resource "docker_container" "app_container" {
+ attach = false
+ bridge = (known after apply)
+ command = (known after apply)
+ container_logs = (known after apply)
+ entrypoint = (known after apply)
+ env = (known after apply)
+ exit_code = (known after apply)
+ gateway = (known after apply)
+ hostname = (known after apply)
+ id = (known after apply)
+ image = (known after apply)
+ init = (known after apply)
+ ip_address = (known after apply)
+ ip_prefix_length = (known after apply)
+ ipc_mode = (known after apply)
+ log_driver = "json-file"
+ logs = false
+ must_run = true
+ name = (known after apply)
+ network_data = (known after apply)
+ read_only = false
+ remove_volumes = true
+ restart = "no"
+ rm = false
+ security_opts = (known after apply)
+ shm_size = (known after apply)
+ start = true
+ stdin_open = false
+ tty = false
+ healthcheck {
+ interval = (known after apply)
+ retries = (known after apply)
+ start_period = (known after apply)
+ test = (known after apply)
+ timeout = (known after apply)
}
+ labels {
+ label = (known after apply)
+ value = (known after apply)
}
+ ports {
+ external = 1880
+ internal = 1880
+ ip = "0.0.0.0"
+ protocol = "tcp"
}
+ volumes {
+ container_path = "/data"
+ volume_name = (known after apply)
}
}
# module.container["nodered"].random_string.random[0] will be created
+ resource "random_string" "random" {
+ id = (known after apply)
+ length = 4
+ lower = true
+ min_lower = 0
+ min_numeric = 0
+ min_special = 0
+ min_upper = 0
+ number = true
+ result = (known after apply)
+ special = false
+ upper = false
}
# module.image["grafana"].docker_image.container_image will be created
+ resource "docker_image" "container_image" {
+ id = (known after apply)
+ latest = (known after apply)
+ name = "grafana/grafana"
+ output = (known after apply)
}
# module.image["influxdb"].docker_image.container_image will be created
+ resource "docker_image" "container_image" {
+ id = (known after apply)
+ latest = (known after apply)
+ name = "quay.io/influxdb/influxdb:v2.0.2"
+ output = (known after apply)
}
# module.image["nodered"].docker_image.container_image will be created
+ resource "docker_image" "container_image" {
+ id = (known after apply)
+ latest = (known after apply)
+ name = "nodered/node-red:latest-minimal"
+ output = (known after apply)
}
# module.container["grafana"].module.volume[0].docker_volume.container_volume[0] will be created
+ resource "docker_volume" "container_volume" {
+ driver = (known after apply)
+ id = (known after apply)
+ mountpoint = (known after apply)
+ name = (known after apply)
}
# module.container["grafana"].module.volume[0].docker_volume.container_volume[1] will be created
+ resource "docker_volume" "container_volume" {
+ driver = (known after apply)
+ id = (known after apply)
+ mountpoint = (known after apply)
+ name = (known after apply)
}
# module.container["influxdb"].module.volume[0].docker_volume.container_volume[0] will be created
+ resource "docker_volume" "container_volume" {
+ driver = (known after apply)
+ id = (known after apply)
+ mountpoint = (known after apply)
+ name = (known after apply)
}
# module.container["nodered"].module.volume[0].docker_volume.container_volume[0] will be created
+ resource "docker_volume" "container_volume" {
+ driver = (known after apply)
+ id = (known after apply)
+ mountpoint = (known after apply)
+ name = (known after apply)
}
Plan: 13 to add, 0 to change, 0 to destroy.Changes to Outputs:
+ application_access = [
+ {
+ grafana = {
+ application_access = (known after apply)
}
+ influxdb = {
+ application_access = (known after apply)
}
+ nodered = {
+ application_access = (known after apply)
}
},
]
------------------------------------------------------------------------Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Upon confirmation, terraform apply to build the infrastructure as intended

$ terraform apply --auto-approve
module.container["grafana"].random_string.random[0]: Creating...
module.container["influxdb"].random_string.random[0]: Creating...
module.container["nodered"].random_string.random[0]: Creating...
module.container["grafana"].random_string.random[0]: Creation complete after 0s [id=dk9q]
module.container["nodered"].random_string.random[0]: Creation complete after 0s [id=yyfv]
module.container["influxdb"].random_string.random[0]: Creation complete after 0s [id=nvbv]
module.container["influxdb"].module.volume[0].docker_volume.container_volume[0]: Creating...
module.image["grafana"].docker_image.container_image: Creating...
module.image["nodered"].docker_image.container_image: Creating...
module.image["influxdb"].docker_image.container_image: Creating...
module.container["grafana"].module.volume[0].docker_volume.container_volume[0]: Creating...
module.container["nodered"].module.volume[0].docker_volume.container_volume[0]: Creating...
module.container["grafana"].module.volume[0].docker_volume.container_volume[1]: Creating...
module.container["nodered"].module.volume[0].docker_volume.container_volume[0]: Creation complete after 0s [id=nodered-prod-yyfv-volume-0]
module.container["influxdb"].module.volume[0].docker_volume.container_volume[0]: Creation complete after 0s [id=influxdb-prod-nvbv-volume-0]
module.container["grafana"].module.volume[0].docker_volume.container_volume[1]: Creation complete after 0s [id=grafana-prod-dk9q-volume-1]
module.container["grafana"].module.volume[0].docker_volume.container_volume[0]: Creation complete after 0s [id=grafana-prod-dk9q-volume-0]
module.image["grafana"].docker_image.container_image: Still creating... [10s elapsed]
module.image["nodered"].docker_image.container_image: Still creating... [10s elapsed]
module.image["influxdb"].docker_image.container_image: Still creating... [10s elapsed]
module.image["influxdb"].docker_image.container_image: Creation complete after 19s [id=sha256:86516adf96b152e8da1b29827a54930cd58667596bd643cd298e55189c3c131equay.io/influxdb/influxdb:v2.0.2]
module.image["grafana"].docker_image.container_image: Creation complete after 19s [id=sha256:1d1f9e7812951b3c0ae79066edba26c5ef27d25c5f4fb5b6b37a998659b812adgrafana/grafana]
module.image["nodered"].docker_image.container_image: Still creating... [20s elapsed]
module.image["nodered"].docker_image.container_image: Creation complete after 22s [id=sha256:8c01614b4e0900c1adf6a4d9656e2cabbd89f7699bbb2f51ac2c26f59483b619nodered/node-red:latest-minimal]
module.container["influxdb"].docker_container.app_container[0]: Creating...
module.container["grafana"].docker_container.app_container[0]: Creating...
module.container["nodered"].docker_container.app_container[0]: Creating...
module.container["grafana"].docker_container.app_container[0]: Provisioning with 'local-exec'...
module.container["grafana"].docker_container.app_container[0] (local-exec): Executing: ["/bin/sh" "-c" "echo grafana-prod-dk9q: 172.17.0.3:3000 >> test.txt"]
module.container["grafana"].docker_container.app_container[0]: Creation complete after 2s [id=d73ad9a7f347958c0c7968cb9b9d637df349f5adce66000e4522f566455de51c]
module.container["influxdb"].docker_container.app_container[0]: Provisioning with 'local-exec'...
module.container["influxdb"].docker_container.app_container[0] (local-exec): Executing: ["/bin/sh" "-c" "echo influxdb-prod-nvbv: 172.17.0.2:8086 >> test.txt"]
module.container["influxdb"].docker_container.app_container[0]: Creation complete after 2s [id=053f80300f5ec4744a84294b59e2c8ca886796e03e3348dc5870874102667ac1]
module.container["nodered"].docker_container.app_container[0]: Provisioning with 'local-exec'...
module.container["nodered"].docker_container.app_container[0] (local-exec): Executing: ["/bin/sh" "-c" "echo nodered-prod-yyfv: 172.17.0.4:1880 >> test.txt"]
module.container["nodered"].docker_container.app_container[0]: Creation complete after 2s [id=fe26ff36c167c794c95080eb8a449e4e7ea49d1c3cfdd3ea8eb2a799556767e2]
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.Outputs:application_access = [
{
"grafana" = {
"application_access" = {
"grafana-prod-dk9q" = "172.17.0.3:3000"
}
}
"influxdb" = {
"application_access" = {
"influxdb-prod-nvbv" = "172.17.0.2:8086"
}
}
"nodered" = {
"application_access" = {
"nodered-prod-yyfv" = "172.17.0.4:1880"
}
}
},
]

To access to our apps, please set up our security group for Cloud9 Instance as shown below

Security group settings

Notes: You may set up your selected ip address for best security practice. Also, ports that need to open include 8086 — Influx, 3000 — Grafana, 1880 — Nodered

Now we are heading to create our OpenWeather account

We first subscribe under Current Weather Data

Subscribe current weather data

Then we only need to Get API key under Free tier

Select free tier

Create a new account with your account name, email and password, and checked off those two boxes shown below

Create a new account

Upon creation, heading to API keys and note down the key for future refrence

API key noted down

Now we’ll be setting up our applications across applications.

First, we head to our Influx by accessing ipv4 of your EC2 instance at port 8086

For me, it’s 3.88.49.91:8086

Influx interface

Setting up Influx and note down username, organization name and bucket name for future reference

Influx settings

Notes: Highlighted weather must be provided as Initial Bucket Name for our project to work

We are all set and click Quick Start

Ready to quick start

Load your data

Load your data
Token for influx

Notes: Store this token as well at the side for future reference

Heading back to Explore and locate our weather

Data explorer

By this time, we will only have our weather bucket

Now we will be navigating to your Nodered application by accessing

Nodered interface

Paste below json file into import nodes box 3.88.49.91:1880 Again, it should be your EC2 public ipv4 at port 1880

nodered-flow.json

[{"id":"60a93e77.2fa18","type":"tab","label":"API Call","disabled":false,"info":""},{"id":"75924f56.e2a3c","type":"influxdb","hostname":"127.0.0.1","port":"8086","protocol":"http","database":"database","name":"derekmtc","usetls":false,"tls":"","influxdbVersion":"2.0","url":"http://172.17.0.3:8086","rejectUnauthorized":true},{"id":"499047ca.862b48","type":"http request","z":"60a93e77.2fa18","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://api.openweathermap.org/data/2.5/weather?q=cape%20canaveral&units=imperial&appid={API-KEY}","tls":"","persist":false,"proxy":"","authType":"","x":270,"y":360,"wires":[["9b8212d1.64cf8"]]},{"id":"ba92047d.ff54e8","type":"inject","z":"60a93e77.2fa18","name":"Get","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"","payloadType":"date","x":110,"y":400,"wires":[["499047ca.862b48","530ab01f.d5867"]]},{"id":"9b8212d1.64cf8","type":"json","z":"60a93e77.2fa18","name":"","property":"payload","action":"","pretty":false,"x":450,"y":420,"wires":[["e6c14187.d5f5"]]},{"id":"ba5f977d.7fd188","type":"function","z":"60a93e77.2fa18","name":"","func":"msg.payload = [\n    [{\n        temperature: msg.weather.main.temp,\n        humidity: msg.weather.main.humidity\n    },\n    {\n        location: msg.weather.name\n    }],\n];\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":600,"y":580,"wires":[["b46f13a2.db485"]]},{"id":"e6c14187.d5f5","type":"change","z":"60a93e77.2fa18","name":"","rules":[{"t":"set","p":"weather","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":500,"wires":[["ba5f977d.7fd188","eab0f4bf.0c0ea8"]]},{"id":"530ab01f.d5867","type":"http request","z":"60a93e77.2fa18","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://api.openweathermap.org/data/2.5/weather?q=houston&units=imperial&appid={API-KEY}","tls":"","persist":false,"proxy":"","authType":"","x":270,"y":460,"wires":[["9b8212d1.64cf8"]]},{"id":"eab0f4bf.0c0ea8","type":"debug","z":"60a93e77.2fa18","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":790,"y":420,"wires":[]},{"id":"b46f13a2.db485","type":"influxdb out","z":"60a93e77.2fa18","influxdb":"75924f56.e2a3c","name":"influxdb","measurement":"current","precision":"","retentionPolicy":"","database":"database","precisionV18FluxV20":"ms","retentionPolicyV18Flux":"","org":"derekmtc","bucket":"weather","x":800,"y":500,"wires":[]}]
Import nodes
Error page

Notes: Since we did not provide influxdb and influxdb out yet, you may encounter error as shown above

Under Palette setting panel, search for influxdb and install it

Locate and install node-red-contrib-influxdb
Nodes installed

Click http request under API Call WorkFlow and replace API-KEY with token created from https://home.openweathermap.org/api_keys

Update http request and replace API-KEY

Input tokens for Influx that we stored previously

Double click Influx to update organization, bucket

Organization and bucket updated

Notes: Make sure the organization and bucket name both will match your Influx info

Influx name and url from cloud9 filled in

Notes: Name needs to match your account name with Influx. Also, URL must be matching with your influx ip address generated from Cloud9

Now click Deploy. If no error occurs, we can move back to our Influx application

Double check our application deployment

Upon double checking, submit

Submit it

We are now all set and ready to check out our data from Grafana. To get access to our Grafana, we need to access 3.88.49.91:3000

Initial credentials:

username: admin

password: admin

Change your password

Create new password if preferred

Select Data Sources under Settings icon

Add data source
Select influxDB

Select Flux under Query Language and under HTTP, provide URL for Influx

Settings

Input your Organization name and token for Influx saved previously

More settings

Input json file for grafana below into below import function under + icon

grafana-dashboard.json

{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 1,
"links": [],
"panels": [
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 14,
"panels": [],
"title": "Space Center Houston: Houston, Tx",
"type": "row"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {},
"mappings": [],
"max": 120,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "#EAB839",
"value": 80
},
{
"color": "red",
"value": 90
}
]
},
"unit": "fahrenheit"
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 4,
"x": 0,
"y": 1
},
"id": 10,
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true,
"text": {}
},
"pluginVersion": "7.4.2",
"targets": [
{
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"orderByTime": "ASC",
"policy": "default",
"query": "from(bucket: \"weather\")\r\n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)\r\n |> filter(fn: (r) =>\r\n r._measurement == \"current\" and\r\n r._field == \"temperature\" and \r\n r.location == \"Houston\"\r\n )",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"title": "Space Center Houston Temperature",
"type": "gauge"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 11,
"w": 8,
"x": 4,
"y": 1
},
"hiddenSeries": false,
"id": 5,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.2",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"orderByTime": "ASC",
"policy": "default",
"query": "from(bucket: \"weather\")\r\n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)\r\n |> filter(fn: (r) =>\r\n r._measurement == \"current\" and\r\n r._field == \"temperature\" and \r\n r.location == \"Houston\"\r\n )",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Space Center Houston Temperature",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 4,
"x": 12,
"y": 1
},
"id": 8,
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true,
"text": {}
},
"pluginVersion": "7.4.2",
"targets": [
{
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"orderByTime": "ASC",
"policy": "default",
"query": "from(bucket: \"weather\")\r\n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)\r\n |> filter(fn: (r) =>\r\n r._measurement == \"current\" and\r\n r._field == \"humidity\" and \r\n r.location == \"Houston\"\r\n )",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"title": "Space Center Houston Humidity",
"type": "gauge"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 11,
"w": 8,
"x": 16,
"y": 1
},
"hiddenSeries": false,
"id": 4,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.2",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"orderByTime": "ASC",
"policy": "default",
"query": "from(bucket: \"weather\")\r\n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)\r\n |> filter(fn: (r) =>\r\n r._measurement == \"current\" and\r\n r._field == \"humidity\" and \r\n r.location == \"Houston\"\r\n )",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Space Center Houston Humidity",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 12
},
"id": 12,
"panels": [],
"title": "Kennedy Space Center: Cape Canaveral, Fl",
"type": "row"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {},
"mappings": [],
"max": 120,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "#EAB839",
"value": 80
},
{
"color": "red",
"value": 90
}
]
},
"unit": "fahrenheit"
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 4,
"x": 0,
"y": 13
},
"id": 7,
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true,
"text": {}
},
"pluginVersion": "7.4.2",
"targets": [
{
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"orderByTime": "ASC",
"policy": "default",
"query": "from(bucket: \"weather\")\r\n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)\r\n |> filter(fn: (r) =>\r\n r._measurement == \"current\" and\r\n r._field == \"temperature\" and \r\n r.location == \"Cape Canaveral\"\r\n )",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"title": "Kennedy Space Center Temperature",
"type": "gauge"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 11,
"w": 8,
"x": 4,
"y": 13
},
"hiddenSeries": false,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.2",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"orderByTime": "ASC",
"policy": "default",
"query": "from(bucket: \"weather\")\r\n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)\r\n |> filter(fn: (r) =>\r\n r._measurement == \"current\" and\r\n r._field == \"temperature\" and \r\n r.location == \"Cape Canaveral\"\r\n )",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Kennedy Space Center Temperature",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 4,
"x": 12,
"y": 13
},
"id": 9,
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true,
"text": {}
},
"pluginVersion": "7.4.2",
"targets": [
{
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"orderByTime": "ASC",
"policy": "default",
"query": "from(bucket: \"weather\")\r\n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)\r\n |> filter(fn: (r) =>\r\n r._measurement == \"current\" and\r\n r._field == \"humidity\" and \r\n r.location == \"Cape Canaveral\"\r\n )",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"title": "Kennedy Space Center Humidity",
"type": "gauge"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 11,
"w": 8,
"x": 16,
"y": 13
},
"hiddenSeries": false,
"id": 3,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.2",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"groupBy": [
{
"params": [
"$__interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"orderByTime": "ASC",
"policy": "default",
"query": "from(bucket: \"weather\")\r\n |> range(start: v.timeRangeStart, stop:v.timeRangeStop)\r\n |> filter(fn: (r) =>\r\n r._measurement == \"current\" and\r\n r._field == \"humidity\" and \r\n r.location == \"Cape Canaveral\"\r\n )",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Kennedy Space Center Humidity",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"refresh": "5s",
"schemaVersion": 27,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Launch Weather",
"uid": "Qrs6bNyGz",
"version": 5
}

Import after it is loaded

Voila, it’s all set!

Grafana monitoring

Clean Up:

Since we’re with Terraform, terraform destroy will it for us!

$ terraform destroy
.
.
.
Destroy complete! Resources: 13 destroyed.

Then we can tear down our Cloud9 in AWS

Delete Cloud9 Environment

Yes, in a couple of minutes, we tore down the entire infrastructure

Conclusion:

In this project, we truly witnessed the power IaC delivered by Terraform. With customized modules, we could seamlessly set up our infrastructure based on our requirements.

Also, by using AWS Cloud9, we cloud easily review our file infrastructures for Terraform and provision EC2 instance. And security group allows us to open ports for our applications to our own ip address or designated ip address as needed

Last but not least, we have seen how Terraform was able to build up the whole infrastructure in a couple of mins and update seamlessly. At the end of the day, tear down the entire infrastructure in a couple of minutes without human error

This project is only for learning purpose. This original project is from Udemy Course Instructor Derek Morgan https://courses.derekops.com/terraform, github source for his full course: https://github.com/morethancertified/mtc-terraform.

--

--

Paul Zhao

Amazon Web Service Certified Solutions Architect Professional & Devops Engineer