Cloudwatch alarm creation fails due to heredoc - terraform

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

Related

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

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}"

Terraform syntax for putting json as value in a map

I'm new to terraform. I have a json object that I need to set as the value in a terraform map so that the resource gets created with the json as the value.
The .tf file looks like this in that section:
...
config_overrides = {
override_1 = "True"
override_2 = '{"key1":"val1","key2":"val2"}' #this is the json object
}
...
However, the terraform lint command terraform lint -check is failing on the json object.
$ terraform fmt -check
Error: Invalid character
on myterraform.tf line 28, in resource <<resource name>> :
28: override_2 = '{"key1":"val1","key2":"val2"}'
Single quotes are not valid. Use double quotes (") to enclose strings.
Error: Invalid expression
on myterraform.tf line 28, in resource <<resource name>>:
28: override_2 = '{"key1":"val1","key2":"val2"}'
Expected the start of an expression, but found an invalid expression token.
I have tried many different variations and cant get the linter to accept it. Please advise.
You can use Terraform's jsonencode function so that Terraform itself is responsible for generating the JSON and you only need to worry about the data structure:
override_2 = jsonencode({
"key1": "val1",
"key2": "val2",
})
Terraform's object expression syntax happens to be similar to JSON's and so the argument to jsonencode here looks a lot like the JSON string it'll convert to, but that is really just a normal Terraform expression and so you can include any Terraform expression constructs in there. For example:
override_2 = jsonencode({
"key1": "val1",
"key2": var.any_variable,
})
You will need to use the \ in the value ' isn't going to work.
config_overrides = {
override_1 = "True"
override_2 = "{\"key1\":\"val1\",\"key2\":\"val2\"}"
}

Convert string or array to map in Terraform?

Terraform v0.10.7
AWS provider version = "~> 1.54.0"
Are there any examples how to translate a string or list into a map in Terraform?
We are setting up Consul key/value store like this:
consul kv put common/rules/alb/service1 name=service1,port=80,hcproto=http,hcport=80
I can access keys and values properly, and now I am trying to use values as a map in Terraform:
data "consul_key_prefix" "common" {
path_prefix = "common/rules"
}
output "common"{
value = "${jsonencode(lookup(var.CommonRules,element(keys(var.CommonRules),1))) }"
}
$ terraform output
common = "{name=service1,port=80,hcproto=http,hcport=80}"
But when I try to access it as a map, it doesn't work:
output "common"{
value = "${lookup(jsonencode(lookup(var.CommonRules,element(keys(var.CommonRules),1))),"name") }"
}
$ terraform output
(no response)
I tried few things here - e.g. splitting these values and joining them again into a list, and then running "map" function but it doesn't work either:
$ terraform output
common = [
name,
service1,
port,
80,
hcproto,
http,
hcport,
80
]
and then trying to create map of that list:
output "common2" {
value = "${map(split(",",join(",",split("=",lookup(var.CommonRules,element(keys(var.CommonRules),1))))))}"
}
but it doesn't work either.
So my question would be - does anyone has working example where he did translated string (or list) into a map?
Thanks in advance.
jsondecode function in upcoming Terraform v0.12 would be the tool to solve this problem.
jsondecode function github issue

Terraform Module - Output error when count = 0

I'm relatively new to Terraform - I have a module setup as below, the issue I'm having is with the outputs if the module count is '0' when running a terraform plan. Output PW works fine now that I've used the element(concat workaround but the Output I'm having issues with is DCPWUn, I get the following error:
Error: Error refreshing state: 1 error(s) occurred:
* module.PrimaryDC.output.DCPWUn: At column 21, line 1: rsadecrypt: argument 1 should be type string, got type list in:
${element(concat("${rsadecrypt(aws_spot_instance_request.PrimaryDC.*.password_data,file("${var.PATH_TO_PRIVATE_KEY}"))}", list("")), 0)}
Code:
resource "aws_spot_instance_request" "PrimaryDC" {
wait_for_fulfillment = true
provisioner "local-exec" {
command = "aws ec2 create-tags --resources ${self.spot_instance_id} --tags Key=Name,Value=${var.ServerName}0${count.index +01}"
}
ami = "ami-629a7405"
spot_price = "0.01"
instance_type = "t2.micro"
count = "${var.count}"
key_name = "${var.KeyPair}"
subnet_id = "${var.Subnet}"
vpc_security_group_ids = ["${var.SecurityGroup}"]
get_password_data = "true"
user_data = <<EOF
<powershell>
Rename-computer -NewName "${var.ServerName}0${count.index +01}"
</powershell>
EOF
tags {
Name = "${var.ServerName}0${count.index +01}"
}
}
output "PW" {
value = "${element(concat("${aws_spot_instance_request.PrimaryDC.*.password_data}", list("")), 0)}"
}
output "DCPWUn" {
value = "${element(concat("${rsadecrypt(aws_spot_instance_request.PrimaryDC.*.password_data,file("${var.PATH_TO_PRIVATE_KEY}"))}", list("")), 0)}"
}
As the error says, rsadecrypt has an argument that is of type list, not string as it should be. If you want to ensure that the argument is a string, you need to invert your function call nesting to make sure that rsadecrypt gets a string:
output "DCPWUn" {
value = "${rsadecrypt(element(concat(aws_spot_instance_request.PrimaryDC.*.password_data, list("")), 0),file("${var.PATH_TO_PRIVATE_KEY}"))}"
}
The problem lies within this line
${element(concat("${rsadecrypt(aws_spot_instance_request.PrimaryDC.*.password_data,file("${var.PATH_TO_PRIVATE_KEY}"))}", list("")), 0)}
What are you trying to achieve? Let's break it down a little
element(…, 0): Get the first element of the following list.
concat(…,list("")): Concatenate the following list of strings and then append the concatenation of a list containing the empty string (Note that the second part is not useful, since you are appending an empty string).
rsadecrypt(…,file("${var.PATH_TO_PRIVATE_KEY}")): decrypt the following expression with the private key (Error: The following thing needs to be a string, you will be supplying a list)
aws_spot_instance_request.PrimaryDC.*.password_data This is a list of all password data (and not a string).
I don't know what your desired output should look like, but with the above list, you may be able to mix-and-match the functions to suit your needs.
edit: Fixed a mistake thanks to the comment by rahuljain1311.

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