Is there a way to have division when writing terraform code for a log alert in Datadog? - terraform

I want have a terraform code to create a Datadog monitor for the percentage of errors in logs compared with all of them.
This is what I've tried
resource "datadog_monitor" "log_errors_count" {
count = local.memory_usage_threshold.critical \> 0 ? 1 : 0
name = "\[${module.label.id}\] ${length(var.description) \> 0 ? var.description : "Log Errors Percentage"}"
type = "log alert"
query = "logs(\"service:api-member status:error\").index(\"*\").rollup(\"count\").by(\"service\").last(\"${var.period}\") / logs(\"service:api-member\").index(\"*\").rollup(\"count\").by(\"service\").last(\"${var.period}\") \> ${local.logged_errors_threshold.critical}"
monitor_thresholds {
ok = local.logged_errors_threshold.ok
warning = local.logged_errors_threshold.warning
critical = local.logged_errors_threshold.critical
}
}
But it returns:
400 Bad Request: {"errors":["The value provided for parameter 'query' is invalid: invalid operator specified: "]}
I have done this kind of division for a metric alert and it worked fine. Using Datadog dashboard I can create a log monitor the way I want, but it looks like I am missing something when I try to do it using terraform.

Try to escape the internal quotes with a backslash
query = "logs(/"service:api-member status:error/").index(/"/").rollup/"count/").by(/"service/").last(/"${var.period}/") / logs(/"service:api-member/").index(/"*"/).rollup(/"count/").by(/"service/").last(/"${var.period}/") \> ${local.logged_errors_threshold.critical}"

Related

Print each value from terraform list variable

I have changed this datadog terraform module to support multiple slack channels, basically changed var.alert_channel from string to list(string) How would I print all the slack channels in the array in the last line of the message block?
resource "monitor" "high_error_rate" {
count = length(var.environment)
name = "${var.name} (${upper(var.environment[count.index])}) high error rate"
type = "log alert"
message = <<EOF
${var.name} has increased error log rate throughout last 5 mins
Environment: ${upper(var.environment[count.index])}
#slack-${var.alert_channel}
EOF
What would be the best way to print #slack-${var.alert_channel} for each channel in the list?
I looked into count and for each, but didn't found a working solution for this yet

Cloudwatch alarm creation fails due to heredoc

I am trying to create a composite cloudwatch alarm using terraform. But unfortunately my terraform code breaks with the following error:
Error: error creating CloudWatch Composite Alarm
(node-count-office-time-composite-alarm-DP-1474-desert):
ValidationError: AlarmRule must not contain leading or trailing
whitespace or be null
status code: 400, request id: 272b14ae-e6bd-4e65-8bb8-25372d9a5f7c
Following is my terraform code:
resource "aws_cloudwatch_composite_alarm" "node_count_office_time_alarm" {
depends_on = [aws_cloudwatch_metric_alarm.node_count, aws_cloudwatch_metric_alarm.office_time]
alarm_description = "Composite alarm for node count & office time"
alarm_name = "node-count-office-time-composite-alarm-${local.postfix}"
alarm_actions = [var.sns_topic_arn]
ok_actions = [var.sns_topic_arn]
alarm_rule =<<-EOF
ALARM(${aws_cloudwatch_metric_alarm.node_count.alarm_name}) AND
ALARM(${aws_cloudwatch_metric_alarm.office_time.alarm_name})
EOF
}
I checked many times and there are no leading or trailing spaces in my alarm_rule. Only new line after AND operator. I am using terraform 0.15.3 version. Anyone faces similar issues and how can I resolve this issue? thanks
I did not find the solution to how to make the heredoc working. But I fixed it for the time being using direct string expression instead of heredoc block. Following is the string expression:
alarm_rule = "ALARM(${aws_cloudwatch_metric_alarm.node_count.alarm_name}) AND ALARM(${aws_cloudwatch_metric_alarm.office_time.alarm_name})"
I hope it is useful for others if they face the same issue. thanks
Terraform instructions https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_composite_alarm are not accurate as of this writing in 2021.
alarm_rule accepts a single string argument, EOF/heredoc has to be processed to create a literal string:
locals {
alarm_rule_with_newlines = <<-EOF
ALARM(${aws_cloudwatch_metric_alarm.alpha.alarm_name}) OR
ALARM(${aws_cloudwatch_metric_alarm.bravo.alarm_name})
EOF
}
[...]
alarm_rule = trimspace(replace(local.alarm_rule_with_newlines, "/\n+/", " "))
I was not satisfied with neither of proposed answers so I have another solution.
Move your composite alert rules to separate file and just read it:
alarm_rule = file("./composite-alert-rule")
or
alarm_rule = templatefile("./composite-alert-rule", { arg = ... })
if you need to pass some dynamic args.
Check terraform docs for reference:
https://www.terraform.io/language/functions/templatefile
https://www.terraform.io/language/functions/file

Terraform Datadog Query Is Invalid

I'm trying to test out creating a monitor for google pub sub and am getting an "Invalid Query" error. This is the query text when i view source of another working monitor, so i'm confused as to why this isn't working.
Error: Error: error creating monitor: 400 Bad Request: {"errors":["The value provided for parameter 'query' is invalid"]}
Terraform:
resource "datadog_monitor" "bad_stuff_sub_monitor" {
name = "${var.customer_name} Bad Stuff Monitor"
type = "metric alert"
message = "${var.customer_name} Bad Stuff Topic getting too big. Notify: ${var.datadog_monitor_notify_list}"
escalation_message = "Escalation message #pagerduty"
query = "avg:gcp.pubsub.subscription.num_undelivered_messages{project_id:terraform_gcp_test}"
thresholds = {
ok = 0
warning = 1
warning_recovery = 0
critical = 2
critical_recovery = 1
}
notify_no_data = false
renotify_interval = 1440
notify_audit = false
timeout_h = 60
include_tags = true
# ignore any changes in silenced value; using silenced is deprecated in favor of downtimes
lifecycle {
ignore_changes = [silenced]
}
tags = [var.customer_name, var.project_name]
}
So I ended up just looking at the tests in the datadog terraform provider and noticing the query format they are testing.
query = "avg(last_30m):avg:gcp.pubsub.subscription.num_undelivered_messages{project_id:${var.project_name},subscription_id:{project_id:terraform_gcp_test} > 2"
It seems you need to specify a time range and also add in a comparison threshold that matches your critical alert threshold. That was what was missing.

Terraform - How to use conditionally created resource's output in conditional operator?

I have a case where I have to create an aws_vpc resource if the user does not provide vpc id. After that I am supposed to create resources with that VPC.
Now, I am applying conditionals while creating an aws_vpc resource. For example, only create VPC if existing_vpc is false:
count = "${var.existing_vpc ? 0 : 1}"
Next, for example, I have to create nodes in the VPC. If the existing_vpc is true, use the var.vpc_id, else use the computed VPC ID from aws_vpc resource.
But, the issue is, if existing_vpc is true, aws_vpc will not create a new resource and the ternary condition is anyways trying to check if the aws_vpc resource is being created or not. If it doesn't get created, terraform errors out.
An example of the error when using conditional operator on aws_subnet:
Resource 'aws_subnet.xyz-subnet' not found for variable 'aws_subnet.xyz-subnet.id'
The code resulting in the error is:
subnet_id = "${var.existing_vpc ? var.subnet_id : aws_subnet.xyz-subnet.id}"
If both things are dependent on each other, how can we create conditional resources and assign values to other configuration based on them?
You can access dynamically created modules and resources as follows
output "vpc_id" {
value = length(module.vpc) > 0 ? module.vpc[*].id : null
}
If count = 0, output is null
If count > 0, output is list of vpc ids
If count = 1 and you want to receive a single vpc id you can specify:
output "vpc_id" {
value = length(module.vpc) > 0 ? one(module.vpc).id : null
}
The following example shows how to optionally specify whether a resource is created (using the conditional operator), and shows how to handle returning output when a resource is not created. This happens to be done using a module, and uses an object variable's element as a flag to indicate whether the resource should be created or not.
But to specifically answer your question, you can use the conditional operator as follows:
output "module_id" {
value = var.module_config.skip == true ? null : format("%v",null_resource.null.*.id)
}
And access the output in the calling main.tf:
module "use_conditionals" {
source = "../../scratch/conditionals-modules/m2" # << Change to your directory
a = module.skipped_module.module_id # Doesn't exist, so might need to handle that.
b = module.notskipped_module.module_id
c = module.default_module.module_id
}
Full example follows. NOTE: this is using terraform v0.14.2
# root/main.tf
provider "null" {}
module "skipped_module" {
source = "../../scratch/conditionals-modules/m1" # << Change to your directory
module_config = {
skip = true # explicitly skip this module.
name = "skipped"
}
}
module "notskipped_module" {
source = "../../scratch/conditionals-modules/m1" # << Change to your directory
module_config = {
skip = false # explicitly don't skip this module.
name = "notskipped"
}
}
module "default_module" {
source = "../../scratch/conditionals-modules/m1" # << Change to your directory
# The default position is, don't skip. see m1/variables.tf
}
module "use_conditionals" {
source = "../../scratch/conditionals-modules/m2" # << Change to your directory
a = module.skipped_module.module_id
b = module.notskipped_module.module_id
c = module.default_module.module_id
}
# root/outputs.tf
output skipped_module_name_and_id {
value = module.skipped_module.module_name_and_id
}
output notskipped_module_name_and_id {
value = module.notskipped_module.module_name_and_id
}
output default_module_name_and_id {
value = module.default_module.module_name_and_id
}
the module
# m1/main.tf
resource "null_resource" "null" {
count = var.module_config.skip ? 0 : 1 # If skip == true, then don't create the resource.
provisioner "local-exec" {
command = <<EOT
#!/usr/bin/env bash
echo "null resource, var.module_config.name: ${var.module_config.name}"
EOT
}
}
# m1/variables.tf
variable "module_config" {
type = object ({
skip = bool,
name = string
})
default = {
skip = false
name = "<NAME>"
}
}
# m1/outputs.tf
output "module_name_and_id" {
value = var.module_config.skip == true ? "SKIPPED" : format(
"%s id:%v",
var.module_config.name,
null_resource.null.*.id
)
}
output "module_id" {
value = var.module_config.skip == true ? null : format("%v",null_resource.null.*.id)
}
The current answers here are helpful when you are working with more modern versions of terraform, but as noted by OP here they do not work when you are working with terraform < 0.12 (If you're like me and still dealing with these older versions, I am sorry, I feel your pain.)
See the relevant issue from the terraform project for more info on why the below is necessary with the older versions.
but to avoid link rot, I'll use the OPs example subnet_id argument using the answers in the github issue.
subnet_id = "${element(compact(concat(aws_subnet.xyz-subnet.*.id, list(var.subnet_id))),0)}"
From the inside out:
concat will join the splat output list to list(var.subnet_id) -- per the background link 'When count = 0, the "splat syntax" expands to an empty list'
compact will remove the empty item
element will return your var.subnet_id only when compact recieves the empty splat output.

What is the terraform syntax to create an AWS Route53 TXT record that has a map as JSON as payload?

My intention is to create an AWS Route53 TXT record, that contains a JSON representation of a terraform map as payload.
I would expect the following to do the trick:
variable "payload" {
type = "map"
default = {
foo = "bar"
baz = "qux"
}
}
resource "aws_route53_record" "TXT-json" {
zone_id = "${module.domain.I-zone_id}"
name = "test.${module.domain.I-fqdn}"
type = "TXT"
ttl = "${var.ttl}"
records = "${list(jsonencode(var.payload))}"
}
terraform validate and terraform plan are ok with that. terraform apply starts happily, but AWS reports an error:
* aws_route53_record.TXT-json: [ERR]: Error building changeset: InvalidChangeBatch: Invalid Resource Record: FATAL problem: InvalidCharacterString (Value should be enclosed in quotation marks) encountered with '"{"baz":"qux","foo":"bar"}"'
status code: 400, request id: 062d4536-3ad3-11e7-af24-0fbcd067fb9e
Terraform version is
Terraform v0.9.4
String handling is very difficult in HCL. I found many references surrounding this issue on the 'net, but I can't seem to find the actual solution. A solution based on the workaround noted in terraform#10048 doesn't work. "${list(substr(jsonencode(var.payload), 1, -1))}" removes the starting curly brace {, not the first quote. That seems to be added later.
Adding quotes (as the error message from AWS suggests) doesn't help; it just adds more quotes, and there already are (the AWS error message is misleading).
The message you're getting is not generated by Terraform. It is a validation error raised by Route53. You'd get the same error if you added eg. {"a":2,"foo":"bar"} as value via the AWS console.
On the other hand, escaping the JSON works ie. I was able to add "{\"a\":2,\"foo\":\"bar\"}" as a TXT value through the AWS console.
If you're OK with that, you can perform a double jsonencode, meaning that you can jsonencode the JSON string generated by jsonencode such as:
variable "payload" {
type = "map"
default = {
foo = "bar"
baz = "qux"
}
}
output "test" {
value = "${jsonencode(jsonencode(var.payload))}"
}
which resolves to:
➜ ~ terraform apply
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
test = "{\"baz\":\"qux\",\"foo\":\"bar\"}"
(you would of course have to use the aws_route53_record resource instead of output)
so basically this works:
resource "aws_route53_record" "record_txt" {
zone_id = "${data.aws_route53_zone.primary.zone_id}"
name = "${var.my_domain}"
type = "TXT"
ttl = "300"
records = ["{\\\"my_value\\\", \\\"${var.my_value}\\\"}"]
}
U're welcome.

Resources