Photo by Yassine Khalfalli on Unsplash
Cheating with Terraform State Show
Using terraform import and terraform state show to generate Terraform for resources created elsewhere.
Background
One of the cards I took on last week, was the task of adding a low-no-data
alert for one of our services. This was an Action Item from an incident post-mortem when we were manually alerted to one of our endpoints being down. Having this alert would ensure that we are notified more quickly should this happen again.
Using Lightstep, it was pretty easy to create the alert based on a metric that we had. (We use a different metric IRL for traffic, but for the sake of this tutorial, I'm using scrape_samples_scraped
)
But I wasn't quite done yet.
What about making the same alert in all the other
environments
? I would need to recreate this alert for all of those environments by hand.What happens if we have the worst day ever ™️ and all of our alerts are gone? I would need to remember the queries and details to recreate them.
This is where Infrastructure-as-Code comes in handy, and specifically Terraform - which allows us to codify our resources such as lightstep_alert
's. By using Terraform, we can:
Focus on being DRY (Don't Repeat Yourself) - Create one alert and programmatically generate alerts in our other environments.
Have our alerts codified - Be able to recover from a disaster and restore to our current state.
And so I set out to write the Terraform with a file that looked like this:
resource "lightstep_alert" "low-no-requests-api-terraform" {
}
Staring at this blank resource stanza made me think:
But wait...I already created the alert in the UI..do I now need to flip back and forth between the Terraform provider documentation and re-create the alert in Terraform?
This would be like the equivalent of getting a transparency paper and attempting to redraw a reference image on a different piece of paper.
I thought this was the only way until I learned a very valuable pattern from one of my very wise coworkers.
[You already have the alert,] Why don't you just
import
the alert and then usestate show
to get the code for it?
I..had never thought of that. But, I tried it and it worked! And was a huge time saver.
Here's the tutorial.
Tutorial
Set up Teraform Provider
First up was to ensure I had set up my Terraform provider to use my Lightstep organization
and an api_key
with a minimum level of Member
provider "lightstep" {
api_key = var.ls_api_key
organization = "LightStep"
}
Get ID of previously created Resource
To import the lightstep_alert
, I needed need to grab the ID of the existing alert. (This can be easily gathered from the browser, see below).
Import the Resource
With the alert ID in hand, I simply had to run a terraform import lightstep_alert.<name_of_alert> <project>.<id>
- In my example it was:
terraform import lightstep_alert.low-no-requests-api dev-tratnayake.mXgC4cG1f
terraform import lightstep_alert.low-no-requests-api dev-tratnayake.mXgC4cG1f 1 ✘ at 20:34:20
lightstep_alert.low-no-requests-api: Importing from ID "dev-tratnayake.mXgC4cG1f"...
lightstep_alert.low-no-requests-api: Import prepared!
Prepared lightstep_alert for import
lightstep_alert.low-no-requests-api: Refreshing state... [id=mXgC4cG1f]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
Show the imported Resource
Now here's the magic part where you can use Terraform like a 🔬 microscope 🔬 to break down an existing resource into its Terraform. With the resource imported into Terraform state, I could simply use terraform state show
to output the alert in Terraform.
terraform state show lightstep_alert.low-no-requests-api 1 ✘ at 20:35:27
# lightstep_alert.low-no-requests-api:
resource "lightstep_alert" "low-no-requests-api" {
id = "mXgC4cG1f"
name = "Low-no-data-alert (UI)"
project_name = "dev-tratnayake"
type = "metric_alert"
expression {
is_multi = false
is_no_data = false
operand = "below"
thresholds {
critical = "1"
warning = "5000"
}
}
query {
display = "line"
hidden = false
hidden_queries = {}
query_name = "a"
query_string = "metric scrape_samples_scraped | filter (job == \"apiserver\") | latest | group_by [\"job\"], sum"
}
}
Use the Code from the imported Resource
With the alert now codified, I could simply copy-pasta it as a new alert in my Terraform file as follows (ensuring to strip out the id
and metric_type
as those are computed when the Terraform is applied).
resource "lightstep_alert" "low-no-requests-api-terraform" {
# id = "mXgC4cG1f"
name = "Low-no-data-alert (Terraform)"
project_name = "dev-tratnayake"
# type = "metric_alert"
[... Rest of the Terraform from `terraform state show` here ... ]
Apply the Terraform to create the Resource
Finally, I ran a terraform apply
which would:
Create the new alert (
low-no-requests-api-terraform
); andDelete the old alert created via UI (
low-no-requests
) because that's not in the Terraform file, and therefore doesn't match the Terraform state.
terraform apply ✔ at 20:37:03
lightstep_alert.low-no-requests-api: Refreshing state... [id=mXgC4cG1f]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
- destroy
Terraform will perform the following actions:
# lightstep_alert.low-no-requests-api will be destroyed
# (because lightstep_alert.low-no-requests-api is not in configuration)
- resource "lightstep_alert" "low-no-requests-api" {
- id = "mXgC4cG1f" -> null
- name = "Low-no-data-alert (UI)" -> null
- project_name = "dev-tratnayake" -> null
- type = "metric_alert" -> null
- expression {
- is_multi = false -> null
- is_no_data = false -> null
- operand = "below" -> null
- thresholds {
- critical = "1" -> null
- warning = "5000" -> null
}
}
- query {
- display = "line" -> null
- hidden = false -> null
- hidden_queries = {} -> null
- query_name = "a" -> null
- query_string = "metric scrape_samples_scraped | filter (job == \"apiserver\") | latest | group_by [\"job\"], sum" -> null
}
}
# lightstep_alert.low-no-requests-api-terraform will be created
+ resource "lightstep_alert" "low-no-requests-api-terraform" {
+ id = (known after apply)
+ name = "Low-no-data-alert (Terraform)"
+ project_name = "dev-tratnayake"
+ type = (known after apply)
+ expression {
+ is_multi = false
+ is_no_data = false
+ operand = "below"
+ thresholds {
+ critical = "1"
+ warning = "5000"
}
}
+ query {
+ display = "line"
+ hidden = false
+ query_name = "a"
+ query_string = "metric scrape_samples_scraped | filter (job == \"apiserver\") | latest | group_by [\"job\"], sum"
}
}
Plan: 1 to add, 0 to change, 1 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
lightstep_alert.low-no-requests-api: Destroying... [id=mXgC4cG1f]
lightstep_alert.low-no-requests-api-terraform: Creating...
lightstep_alert.low-no-requests-api: Destruction complete after 1s
lightstep_alert.low-no-requests-api-terraform: Creation complete after 2s [id=mw5HVFtch]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed
The end result is the Alert that I created in UI - now codified and managed by Terraform ☑️
Conclusion
There are many times that Terraform is not the first thing we reach for when creating a resource. Many times we may reach for a GUI or specialized tool to get create resources, especially during rapid prototyping. Using terraform import
and terraform state show
allows you to keep that momentum by using Terraform to codify the resources you've already created (in a more user-friendly tool) which can then easily be modified to fit your needs.
Doing this is a huge time-saver that almost felt like cheating, and I will definitely be using this in my future Terraforming travels 🚀