Terraform interpolation to json file when json requires value to be integer - terraform

Trying to work out if this is possible or not. Trawled terraform docs to no avail (not much surprise there).
Take the below extremely slim line example.
[
{
"cpu": "${var.master_container_cpu}",
}
]
Adjoined to this tf parameter when invoking aws_ecs_task_definition resource;
container_definitions = "${file("task-definitions/example.json")}"
Will result in the following error;
Error: aws_ecs_task_definition.example-task: ECS Task Definition container_definitions is invalid: Error decoding JSON: json: cannot unmarshal string into Go struct field ContainerDefinition.Cpu of type int64
any help more than welcome :)

It looks like you should use a template to compile the JSON before using in the definition
data "template_file" "task" {
template = "${file("${task-definitions/example.json")}"
vars {
cpu = "${var.master_container_cpu}"
}
}
In the JSON file you can reference the var using ${cpu}
Then you are able to use the output as your definition
container_definitions = "${data.template_file.task.rendered}"

Related

How do I apply a CRD from github to a cluster with terraform?

I want to install a CRD with terraform, I was hoping it would be easy as doing this:
data "http" "crd" {
url = "https://raw.githubusercontent.com/kubernetes-sigs/application/master/deploy/kube-app-manager-aio.yaml"
request_headers = {
Accept = "text/plain"
}
}
resource "kubernetes_manifest" "install-crd" {
manifest = data.http.crd.body
}
But I get this error:
can't unmarshal tftypes.String into *map[string]tftypes.Value, expected
map[string]tftypes.Value
Trying to convert it to yaml with yamldecode also doesn't work because yamldecode doesn't support multi-doc yaml files.
I could use exec, but I was already doing that while waiting for the kubernetes_manifest resource to be released. Does kubernetes_manifest only support a single resource or can it be used to create several from a raw text manifest file?
kubernetes_manifest (emphasis mine)
Represents one Kubernetes resource by supplying a manifest attribute
That sounds to me like it does not support multiple resources / a multi doc yaml file.
However you can manually split the incoming document and yamldecode the parts of it:
locals {
yamls = [for data in split("---", data.http.crd.body): yamldecode(data)]
}
resource "kubernetes_manifest" "install-crd" {
count = length(local.yamls)
manifest = local.yamls[count.index]
}
Unfortunately on my machine this then complains about
'status' attribute key is not allowed in manifest configuration
for exactly one of the 11 manifests.
And since I have no clue of kubernetes I have no idea what that means or wether or not it needs fixing.
Alternatively you can always use a null_resource with a script that fetches the yaml document and uses bash tools or python or whatever is installed to convert and split and filter the incoming yaml.
I got this to work using kubectl provider. Eventually kubernetes_manifest should work as well, but it is currently (v2.5.0) still beta and has some bugs. This example only uses kind+name, but for full uniqueness, it should also include the API and the namespace params.
resource "kubectl_manifest" "cdr" {
# Create a map { "kind--name" => yaml_doc } from the multi-document yaml text.
# Each element is a separate kubernetes resource.
# Must use \n---\n to avoid splitting on strings and comments containing "---".
# YAML allows "---" to be the first and last line of a file, so make sure
# raw yaml begins and ends with a newline.
# The "---" can be followed by spaces, so need to remove those too.
# Skip blocks that are empty or comments-only in case yaml began with a comment before "---".
for_each = {
for pair in [
for yaml in split(
"\n---\n",
"\n${replace(data.http.crd.body, "/(?m)^---[[:blank:]]*(#.*)?$/", "---")}\n"
) :
[yamldecode(yaml), yaml]
if trimspace(replace(yaml, "/(?m)(^[[:blank:]]*(#.*)?$)+/", "")) != ""
] : "${pair.0["kind"]}--${pair.0["metadata"]["name"]}" => pair.1
}
yaml_body = each.value
}
Once Hashicorp fixes kubernetes_manifest, I would recommend using the same approach. Do not use count+element() because if the ordering of the elements change, Terraform will delete/recreate many resources without needed it.
resource "kubernetes_manifest" "crd" {
for_each = {
for value in [
for yaml in split(
"\n---\n",
"\n${replace(data.http.crd.body, "/(?m)^---[[:blank:]]*(#.*)?$/", "---")}\n"
) :
yamldecode(yaml)
if trimspace(replace(yaml, "/(?m)(^[[:blank:]]*(#.*)?$)+/", "")) != ""
] : "${value["kind"]}--${value["metadata"]["name"]}" => value
}
manifest = each.value
}
P.S. Please support Terraform feature request for multi-document yamldecode. Will make things far easier than the above regex.
Terraform can split a multi-resource yaml (---) for you (docs):
# fetch a raw multi-resource yaml
data "http" "knative_serving_crds" {
url = "https://github.com/knative/serving/releases/download/knative-v1.7.1/serving-crds.yaml"
}
# split raw yaml into individual resources
data "kubectl_file_documents" "knative_serving_crds" {
content = data.http.knative_serving_crds.body
}
# apply each resource from the yaml one by one
resource "kubectl_manifest" "knative_serving_crds" {
depends_on = [kops_cluster_updater.updater]
for_each = data.kubectl_file_documents.knative_serving_crds.manifests
yaml_body = each.value
}

Reading nested attribute in data source (of a Cloud Run service that might not exist) in Terraform

I'm using Terraform v0.14.4 with GCP. I have a Cloud Run service that won't be managed with Terraform (it might exist or not), and I want to read its url.
If the service exists this works ok:
data "google_cloud_run_service" "myservice" {
name = "myservice"
location = "us-central1"
}
output "myservice" {
value = data.google_cloud_run_service.myservice.status[0].url
}
But if it doesn't exist, I can't get it to work!. What I've tried:
data.google_cloud_run_service.myservice.*.status[*].url
status is null
length(data.google_cloud_run_service.myservice) > 0 ? data.google_cloud_run_service.myservice.*.status[0].url : ""
Tried with join("", data.google_cloud_run_service.myservice.*.status)
I get this error: data.google_cloud_run_service.myservice is object with 9 attributes
coalescelist(data.google_cloud_run_service.myservice.*.status, <...>)
It just returns [null], and using compact over the result gets me a Invalid value for "list" parameter: element 0: string required.
Any ideas?
It seems like you are working against the design of this data source a little here, but based on the error messages you've shown it seems like the behavior is that status is null when the requested object doesn't exist and is a list when it does, and so you'll need to write an expression that can deal with both situations.
Here's my attempt, based only on the documentation of the resource along with some assumptions I'm making based on the error message you included:
output "myservice" {
value = (
data.google_cloud_run_service.myservice.status != null ?
data.google_cloud_run_service.myservice.status[0].url :
null
)
}
There is another potentially-shorter way to write that, relying on the try function's ability to catch dynamic errors and return a fallback value, although this does go against the recommendations in the documentation in that it forces an unfamiliar future reader to do a bit more guesswork to understand in which situations the expression might succeed and which it might return the fallback:
output "myservice" {
value = try(data.google_cloud_run_service.myservice.status[0].url, null)
}

There is no variable named "REGION"

The ec2 launch tempate userdata definition using the template provider is about to be replaced with a templatefile function as it is deprecated.
The definition itself can be done without any problems, but an error occurs during the planning process saying that the processing variables for getting instance metadata etc. are undefined, is there any way to avoid this?
$ terraform version
Terraform v0.13.5
data "template_file" "userdata" {
template = templatefile("${path.module}/userdata.sh.tpl",
vars....
)
}
resource "aws_launch_template" "sample" {
....
user_data = base64encode(data.template_file.userdata.rendered)
....
}
The following definitions are applicable processing
export LOCAL_IP=$(ec2metadata --local-ipv4)
export GLOBAL_IP=$(ec2metadata --public-ipv4)
export REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/[a-z]$//')
export AWS_DEFAULT_REGION="$${REGION}"
$ terraform plan
Error: failed to render : <template_file>:13,30-36: Unknown variable; There is no variable named "REGION"., and 4 other diagnostic(s)
thanks
It doesn't make sense to use the template_file data source and the templatefile function together, because these both do the same thing: render a template.
In your current configuration, you have the templatefile function rendering the template to produce a string, and then the template_file function interpreting that result as if it were a template, and so your template is being evaluated twice.
The first evaluation will turn $${REGION} into ${REGION} as expected, and then the second evaluation will try to handle ${REGION} as a template interpolation.
Instead, remove the template_file data source altogether (it's deprecated) and just use the templatefile function:
user_data = templatefile("${path.module}/userdata.sh.tpl, {
# ...
})
(I removed the base64encode function here because user_data expects an unencoded string value where the provider itself will do the base64 encoding. If you call base64encode yourself here then you'll end up with two levels of base64 encoding, which I imagine is not what you intended.)

Pass complex, non-primitive data types to Terraform template provider

Having a more complex list object like this
variable "proxy" {
type = list(object({
enabled = bool
host = string
port = number
user = string
password = string
}))
default = [
{
enabled = false
host = ""
port = 0
user = ""
password = ""
}
]
}
I want to use this in a external template (cloudinit in my case). The template_file directive allows passing variables to a template. Sadly, not for more complex types:
Note that variables must all be primitives. Direct references to lists or maps will cause a validation error.
So something like this
data "template_file" "cloudinit_data" {
template = file("cloudinit.cfg")
vars = {
proxy = var.proxy
}
}
cause the error
Inappropriate value for attribute "vars": element "proxy": string required.
This leads me to two questions:
How can I pass the variable to the template? I assume that I need to convert it to a primitive type like this:
vars = {
proxy_host = var.proxy.host
}
This doesn't work:
This value does not have any attributes.
Is there an alternative way to pass this object directly to the template?
I'm using v0.12.17.
The template_file data source continues to exist only for compatibility with configurations written for Terraform 0.11. Since you are using Terraform 0.12, you should use the templatefile function instead, which is a built-in part of the language and supports all value types.
Because templatefile is a function, you can call it from anywhere expressions are expected. If you want to use the rendered result multiple times then you could define it as a named local value, for example:
locals {
cloudinit_data = templatefile("${path.module}/cloudinit.cfg", {
proxy = var.proxy
})
}
If you only need this result once -- for example, if you're using it just to populate the user_data of a single aws_instance resource -- then you can just write this expression inline in the resource block, to keep everything together and make the configuration (subjectively) easier to read:
resource "aws_instance" "example" {
# ...
user_data = templatefile("${path.module}/cloudinit.cfg", {
proxy = var.proxy
})
}

Terraform how to interpolate map types in a template_file?

I am trying to pass a map variable into a template_file, and am being thrown this error:
vars (varsname): '' expected type 'string', got unconvertible type 'map[string]interface {}'
data "template_file" "app" {
template = "${file("./app_template.tpl")}"
vars {
container = "${var.container-configuration}"
}
}
variables.tf
variable "container-configuration" {
description = "Configuration for container"
type = "map"
default = {
image = "blahblah.dkr.ecr.us-east-2.amazonaws.com/connect"
container-port = "3000"
host-port = "3000"
cpu = "1024"
memory = "2048"
log-group = "test"
log-region = "us-east-2a"
}
}
Is there a way to pass the map into the template file for interpolation? I haven't found anything clear in the documentation.
Terraform v0.12 introduced the templatefile function, which absorbs the main use-cases of the template_file data source, and accepts values of any type:
templatefile("${path.module}/app_template.tpl", {
container = var.container-configuration
})
Terraform v0.11 and earlier do not have any means to render a template with non-string values. The limitation exists due to the nature of the protocol used to represent map values in the configuration: it is only capable of representing maps of string until the new protocol that was introduced in Terraform v0.12.
Passing maps is not yet supported, see template_file documentation.
From that article:
Variables must all be primitives. Direct references to lists or maps will cause a validation error.
It means you need to pass variables one by one individually.

Resources