Terraform increment resource from existing inventory - azure

Please excuse if this is a dumb question. I'm a terraform noob and trying to determine the best approach to meet an enterprise requirement for resource naming.
Our cloud governance team has determined a naming scheme for all resources where you have [region][resource_type][app_name][instance 0001-999][env] So, for instance we might have something like the following for vm's:
uw1vmmyapp001dev
uw1vmmyapp002dev
etc.
This is all well and good when deploying from scratch as I just use the {count.index} However, now I am trying to determine how to deploy additional resources and start from the previously deloyed resources (that weren't deployed by terraform). Is there a terraform standard for gathering the existing inventory, parsing the current values and starting your incrementing from the highest instance number? (I was using randoms but our cloud governance team squashed that quickly.)
I'm really doing a poor job with my wording. Hopefully this makes some sort of sense?
Oh, I'm using azurerm_virtual_machine

It's going to be pretty difficult when there isn't any delimiting characters... it's just a shoved together string. If there was a delimiting character you could maybe use split to break up the string and find out the number portion. There also doesn't appear to be a data source equivalent of azurerm_virtual_machine to get the naming information anyway.
Given that you'd need to manually look up the name or id anyway to import information about current resources you could find the highest numbered VM then use something like the following to add additional VMs and increment the number:
${var.region}${var.resource_type}${var.appname}${format("%03d", count.index + var.last_num)}${var.env}
To test what this looks like you can look at this example:
variable "last_num" {
default = 98
}
variable "region" {
default = "uw"
}
variable "resource_type" {
default = "vm"
}
variable "appname" {
default = "myapp"
}
variable "env" {
default = "dev"
}
resource "local_file" "foo" {
count = 3
filename = "foo.text"
content = "${var.region}${var.resource_type}${var.appname}${format("%03d", count.index + 1 + var.last_num)}${var.env}"
}
This gives naming output like this:
+ local_file.foo[0]
id: <computed>
content: "uwvmmyapp099dev"
filename: "foo.text"
+ local_file.foo[1]
id: <computed>
content: "uwvmmyapp100dev"
filename: "foo.text"
+ local_file.foo[2]
id: <computed>
content: "uwvmmyapp101dev"
filename: "foo.text"

Related

Is there a condition in terraform same as CloudFormation?

I see people using count to block resource creation in terraform. I want to create some resources if a condition is set to true. Is there such a thing same as in CloudFormation?
You answered yourself, the most similar thing is the count
You can use it combined with a conditional expression, like
resource "x" "y"{
count = var.tag == "to_deploy" ? 1 : 0
}
But this is just a stupid example, you can put everything, also use functions
count = max(var.array) >= 3 ? 1 : 0
And if you need to put a condition on something more complex, you can evaluate to use a locals block where do all elaboration you need, and just use some bool, or what you want, resultant from that in conditional expression.
I would like to help you more, but I should know your specific case, what are the conditions you would have.
In CloudFormation a "condition" is a top-level object type alongside resources, outputs, mappings, etc.
The Terraform language takes a slightly more general approach of just having values of various data types, combining and transforming them using expressions. Therefore there isn't a concept exactly equivalent to CloudFormation's "conditions", but you can achieve a similar effect in other ways using Terraform.
For example, if you want to encode the decision rule in only a single place and then refer to it many times then you can define a Local Value of boolean type and then refer to that from multiple resource blocks. A local value of boolean type is essentially equivalent to a condition object in CloudFormation. The CloudFormation documentation page you linked to has, at the time of writing, an example titled "Simple condition" and the following is a roughly-equivalent version of that example in the Terraform language:
variable "environment_type" {
type = string
validation {
condition = contains(["prod", "test"], var.environment_type)
error_message = "Must be either 'prod' or 'test'."
}
}
locals {
create_prod_resources = (var.environment_type == "prod")
}
resource "aws_instance" "example" {
ami = "ami-0ff8a91507f77f867"
instance_type = "..."
}
resource "aws_ebs_volume" "example" {
count = local.create_prod_resources ? 1 : 0
availability_zone = aws_instance.example.availability_zone
}
resource "aws_volume_attachment" "example" {
count = local.create_prod_resources ? 1 : 0
volume_id = aws_ebs_volume.example[count.index].id
instance_id = aws_instance.example.id
device = "/dev/sdh"
}
Two different resource blocks can both refer to local.create_prod_resources, in the same way that the two resources MountPoint and NewVolume can refer to the shared condition CreateProdResources in the CloudFormation example.

How to solve for_each + "Terraform cannot predict how many instances will be created" issue?

I am trying to create a GCP project with this:
module "project-factory" {
source = "terraform-google-modules/project-factory/google"
version = "11.2.3"
name = var.project_name
random_project_id = "true"
org_id = var.organization_id
folder_id = var.folder_id
billing_account = var.billing_account
activate_apis = [
"iam.googleapis.com",
"run.googleapis.com"
]
}
After that, I am trying to create a service account, like so:
module "service_accounts" {
source = "terraform-google-modules/service-accounts/google"
version = "4.0.3"
project_id = module.project-factory.project_id
generate_keys = "true"
names = ["backend-runner"]
project_roles = [
"${module.project-factory.project_id}=>roles/cloudsql.client",
"${module.project-factory.project_id}=>roles/pubsub.publisher"
]
}
To be honest, I am fairly new to Terraform. I have read a few answers on the topic (this and this) but I am unable to understand how that would apply here.
I am getting the error:
│ Error: Invalid for_each argument
│
│ on .terraform/modules/pubsub-exporter-service-account/main.tf line 47, in resource "google_project_iam_member" "project-roles":
│ 47: for_each = local.project_roles_map_data
│ ├────────────────
│ │ local.project_roles_map_data will be known only after apply
│
│ The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the
│ -target argument to first apply only the resources that the for_each depends on.
Looking forward to learn more about Terraform through this challenge.
With only parts of the configuration visible here I'm guessing a little bit, but let's see. You mentioned that you'd like to learn more about Terraform as part of this exercise, so I'm going to go into a lot of detail about the chain here to explain why I'm recommending what I'm going to recommend, though you can skip to the end if you find this extra detail uninteresting.
We'll start with that first module's definition of its project_id output value:
output "project_id" {
value = module.project-factory.project_id
}
module.project-factory here is referring to a nested module call, so we need to look one level deeper in the nested module terraform-google-modules/project-factory/google//modules/core_project_factory:
output "project_id" {
value = module.project_services.project_id
depends_on = [
module.project_services,
google_project.main,
google_compute_shared_vpc_service_project.shared_vpc_attachment,
google_compute_shared_vpc_host_project.shared_vpc_host,
]
}
Another nested module call! 😬 That one declares its project_id like this:
output "project_id" {
description = "The GCP project you want to enable APIs on"
value = element(concat([for v in google_project_service.project_services : v.project], [var.project_id]), 0)
}
Phew! 😅 Finally an actual resource. This expression in this case seems to be taking the project attribute of a google_project_service resource instance, or potentially taking it from var.project_id if that resource was disabled in this instance of the module. Let's have a look at the google_project_service.project_services definition:
resource "google_project_service" "project_services" {
for_each = local.services
project = var.project_id
service = each.value
disable_on_destroy = var.disable_services_on_destroy
disable_dependent_services = var.disable_dependent_services
}
project here is set to var.project_id, so it seems like either way this innermost project_id output just reflects back the value of the project_id input variable, so we need to jump back up one level and look at the module call to this module to see what that was set to:
module "project_services" {
source = "../project_services"
project_id = google_project.main.project_id
activate_apis = local.activate_apis
activate_api_identities = var.activate_api_identities
disable_services_on_destroy = var.disable_services_on_destroy
disable_dependent_services = var.disable_dependent_services
}
project_id is set to the project_id attribute of google_project.main:
resource "google_project" "main" {
name = var.name
project_id = local.temp_project_id
org_id = local.project_org_id
folder_id = local.project_folder_id
billing_account = var.billing_account
auto_create_network = var.auto_create_network
labels = var.labels
}
project_id here is set to local.temp_project_id, which is declared further up in the same file:
temp_project_id = var.random_project_id ? format(
"%s-%s",
local.base_project_id,
random_id.random_project_id_suffix.hex,
) : local.base_project_id
This expression includes a reference to random_id.random_project_id_suffix.hex, and .hex is a result attribute from random_id, and so its value won't be known until apply time due to how that random_id resource type is implemented. (It generates a random value during the apply step and saves it in the state so it'll stay consistent on future runs.)
This means that (after all of this indirection) module.project-factory.project_id in your module is not a value defined statically in the configuration, and might instead be decided dynamically during the apply step. That means it's not an appropriate value to use as part of the instance key of a resource, and thus not appropriate to use as a key in a for_each map.
Unfortunately the use of for_each here is hidden inside this other module terraform-google-modules/service-accounts/google, and so we'll need to have a look at that one too and see how it's making use of the project_roles input variable. First, let's look at the specific resource block the error message was talking about:
resource "google_project_iam_member" "project-roles" {
for_each = local.project_roles_map_data
project = element(
split(
"=>",
each.value.role
),
0,
)
role = element(
split(
"=>",
each.value.role
),
1,
)
member = "serviceAccount:${google_service_account.service_accounts[each.value.name].email}"
}
There's a couple somewhat-complex things going on here, but the most relevant thing for what we're looking at here is that this resource configuration is creating multiple instances based on the content of local.project_roles_map_data. Let's look at local.project_roles_map_data now:
project_roles_map_data = zipmap(
[for pair in local.name_role_pairs : "${pair[0]}-${pair[1]}"],
[for pair in local.name_role_pairs : {
name = pair[0]
role = pair[1]
}]
)
A little more complexity here that isn't super important to what we're looking for; the main thing to consider here is that this is constructing a map whose keys are built from element zero and element one of local.name_role_pairs, which is declared directly above, along with local.names that it refers to:
names = toset(var.names)
name_role_pairs = setproduct(local.names, toset(var.project_roles))
So what we've learned here is that the values in var.names and the values in var.project_roles both contribute to the keys of the for_each on that resource, which means that neither of those variable values should contain anything decided dynamically during the apply step.
However, we've also learned (above) that the project and role arguments of google_project_iam_member.project-roles are derived from the prefixes of elements in the two lists you provided as names and project_roles in your own module call.
Let's return back to where we started then, with all of this extra information in mind:
module "service_accounts" {
source = "terraform-google-modules/service-accounts/google"
version = "4.0.3"
project_id = module.project-factory.project_id
generate_keys = "true"
names = ["backend-runner"]
project_roles = [
"${module.project-factory.project_id}=>roles/cloudsql.client",
"${module.project-factory.project_id}=>roles/pubsub.publisher"
]
}
We've learned that names and project_roles must both contain only static values decided in the configuration, and so it isn't appropriate to use module.project-factory.project_id because that won't be known until the random project ID has been generated during the apply step.
However, we also know that this module is expecting the prefix of each item in project_roles (the part before the =>) to be a valid project ID, so there isn't any other value that would be reasonable to use there.
Therefore we're at a bit of an empasse: this second module has a rather awkward design decision that it's trying to derive a both a local instance key and a reference to a real remote object from the same value, and those two situations have conflicting requirements. But this isn't a module you created, so you can't easily modify it to address that design quirk.
Given that, I see two possible approaches to move forward, neither ideal but both workable with some caveats:
You could take the approach the error message offered as a workaround, asking Terraform to plan and apply the resources in the first module alone first, and then plan and apply the rest on a subsequent run once the project ID is already decided and recorded in the state:
terraform apply -target=module.factory
terraform apply
Although it's annoying to have to do this initial create in two steps, it does at least only matter for the initial creation of this infrastructure. If you update it later then you won't need to repeat this two-step process unless you've changed the configuration in a way that requires generating a new project ID.
While working through the above we saw that this approach of generating and returning a random project ID was optional based on that first module's var.random_project_id, which you set to "true" in your configuration. Without that, the project_id output would be just a copy of your given name argument, which seems to be statically defined by reference to a root module variable.
Unless you particularly need that random suffix on your project ID, you could leave random_project_id unset and thus just get the project ID set to the same static value as your var.project_name, which should then be an acceptable value to use as a for_each key.
Ideally this second module would be designed to separate the values it's using for instance keys from the values it's using to refer to real remote objects, and thus it would be possible to use the random-suffixed name for the remote object but a statically-defined name for the local object. If this were a module under your control then I would've suggested a design change like that, but I assume the current unusual design of that third-party module (packing multiple values into a single string with a delimiter) is a compromise resulting from wanting to retain backward compatibility with an earlier iteration of the module.

Get a list of possible outbound ip addresses in terraform

I'm trying to use the export from a azure function app in terraform to get the possible outbound ip addresses that I can add to a whitelist for a firewall
The parameter returned is a string of ips comma separated.
I have tried using the split function within terraform but it doesn't give a list, it gives an interface which can't be used as a list. I've tried using local scopes to add square brackets around it but still the same.
Let me just add this is terraform 11 not 12.
resource "azurerm_key_vault" "keyvault" {
name = "${var.project_name}-${var.environment}-kv"
location = "${azurerm_resource_group.environment.location}"
resource_group_name = "${azurerm_resource_group.environment.name}"
enabled_for_disk_encryption = true
tenant_id = "${var.tenant_id}"
sku_name = "standard"
network_acls {
bypass = "AzureServices"
default_action = "Deny"
ip_rules = "${split(",", azurerm_function_app.function.possible_outbound_ip_addresses)}"
}
tags = {
asset-code = "${var.storage_tags["asset_code"]}"
module-code = "${var.storage_tags["module_code"]}"
environment = "${var.environment}"
instance-code = "${var.storage_tags["instance_code"]}"
source = "terraform"
}
}
This comes back with the error "ip_rules must be a list".
Thanks
I think what you are seeing here is a classic Terraform 0.11 design flaw: when a value is unknown at plan time (because it will be decided only during apply), Terraform 0.11 can't properly track the type information for it.
Because possible_outbound_ip_addresses is an unknown value at planning time, the result of split with that string is also unknown. Because Terraform doesn't track type information for that result, the provider SDK code rejects that unknown value because it isn't a list.
To address this in Terraform 0.11 requires doing your initial run with the -target argument so that Terraform can focus on creating the function (and thus allocating its outbound IP addresses) first, and then deal with the processing of that string separately once it's known:
terraform apply -target=azurerm_function_app.function
terraform apply # to complete the rest of the work that -target excluded
Terraform 0.12 addressed this limitation by tracking type information for both known and unknown values, so in Terraform 0.12 the split function would see that you gave it an unknown string and accept that as being correctly typed, and then it would return an unknown list of strings to serve as a placeholder for the result that will be finally determined during the apply phase.
If is var.string is 1.2.3.4,5.6.7.8-
split(',', var.string)[0] should give you back 1.2.3.4 as a string. Your questions is difficult without an example.
Here is an example of how I can get a list of possible IPs
create a data source and then a locals var
app_services = [ "app1", "app2", "app3" ]
data "azurerm_app_service" "outbound_ips" {
count = length(var.app_services)
name = var.app_services[count.index]
resource_group_name = var.server_resource_group_name
}
locals {
apps_outbound_ips = distinct(flatten(concat(data.azurerm_app_service.outbound_ips.*.possible_outbound_ip_address_list)))
}
You don't have to use a data source either, if you are building the resource just use the outputs instead of a data source, in my case I use a data source as I build my apps separately.
Works flawlessly for me and produces a list of strings (Strings being each unique outbound IP of the set of app services / function apps) in the form of local.apps_outbound_ips
Enjoy :)

Is it possible to generate a variable name in terraform

So i want to get the variable in the terraform remote state, however we have a number of different one per environment on the shared route53
So for a given environement, we want to pull the zone id out as such;
zone_id = data.terraform_remote_state.route_53.route53_zone_${var.environment}_id
How would I do this please.
In general, it is not possible to use arbitrary dynamic strings as variable names.
However, in this particular case the outputs from terraform_remote_state are collection values and so you can use the index syntax to access a dynamically-built key from your map value:
data.terraform_remote_state.outputs.route53["route53_zone_${var.environment}_id"]
With that said, if possible I would recommend structuring the output values better so that the Route53 zone ids are given as a map by environment, so that this can be obtained in a more intuitive way.
For example, you could make your route53 output be a map of objects whose keys are the environment names:
data.terraform_remote_state.outputs.route53[var.environment].zone_id
output "route53" {
value = tomap({
production = {
zone_id = aws_route53_zone.production.id
}
staging = {
zone_id = aws_route53_zone.staging.id
}
})
}
Or, if you have a variety of different per-environment settings you could structure it as a single output value that is a map of all of those per environment settings keyed by environment name:
data.terraform_remote_state.outputs.environments[var.environment].route53_zone_id
output "environments" {
value = tomap({
production = {
ec2_vpc_id = aws_vpc.production.id
route53_zone_id = aws_route53_zone.production.id
}
staging = {
ec2_vpc_id = aws_vpc.staging.id
route53_zone_id = aws_route53_zone.staging.id
}
})
}
This doesn't change anything about the ultimate result, but grouping things by your environment keys in your outputs is likely to make your intent clearer to future maintainers of these configurations.
(You might also consider whether it'd be better to have a separate configuration/state per environment rather than managing them altogether, but that is a big topic in itself.)

Terraform: How to remove a user resource created from an array/list?

I have a file users.tf file that creates the admin users for aws. It does so by defining a list (e.g. users = ["bob", "john", "tom"])
and then iterating over them with the aws_iam_user resource using the count functionality in terraform, like so:
resource "aws_iam_user" "user" {
count = length(local.users)
name = local.users[count.index]
}
the issue here, is that if I remove the first element of the array ("bob" from the above example), what terraform will suggest to do after issuing terraform plan, instead of deleting bob, is to change bob to john, change john to tom, and delete tom.
like so:
# aws_iam_user.user[0] will be updated in-place
~ resource "aws_iam_user" "user" {
arn = "arn:aws:iam::5555555:user/bob"
force_destroy = false
id = "bob"
~ name = "bob" -> "john"
path = "/"
tags = {}
unique_id = "BLABLABLA11111"
}
# aws_iam_user.user[1] will be updated in-place
~ resource "aws_iam_user" "user" {
arn = "arn:aws:iam::5555555:user/john"
force_destroy = false
id = "john"
~ name = "john" -> "tom"
path = "/"
tags = {}
unique_id = "BLABLABLA22222"
}
# aws_iam_user.user[2] will be destroyed
- resource "aws_iam_user" "user" {
- arn = "arn:aws:iam::5555555:user/tom" -> null
- force_destroy = false -> null
- id = "tom" -> null
- name = "tom" -> null
- path = "/" -> null
- tags = {} -> null
- unique_id = "BLABLABLA3333" -> null
this will result in john getting the arn of bob, and tom getting the arn of john. which is undesirable.
I tried using the very new feature (released 19 hours prior to the writing of this question) of for_each loop instead of count, and defining the keys as the original index numbers, hoping that terraform will consider them as the same resource.
yeah well, no such luck:
...
# aws_iam_user.user[1] will be destroyed
...
# aws_iam_user.user["1"] will be created
...
I will summarize my question:
Is there any way to delete a resource (specifically aws_iam_user), when that resource was created by iterating over a list, such that all the remaining resources stay the way they were?
What you have seen here is the situation that the count documentation warns about in its final paragraph:
Note that the separate resource instances created by count are still identified by their index, and not by the string values in the given list. This means that if an element is removed from the middle of the list, all of the indexed instances after it will see their subnet_id values change, which will cause more remote object changes than were probably intended. The practice of generating multiple instances from lists should be used sparingly, and with due care given to what will happen if the list is changed later.
Fortunately, this is the very problem the for_each feature is intended to solve. Though, in order to use it effectively it's important to choose meaningful unique keys in the map you pass to for_each:
resource "aws_iam_user" "user" {
for_each = { for name in local.users : name => name }
name = each.value
}
This will cause Terraform to track instance identifiers like aws_iam_user.user["john"] rather than aws_iam_user.user[1].
You have existing count-based instances in your state though, so it'll take some migration steps to get there. Unfortunately Terraform doesn't have enough information to automatically correlate your existing index-based addresses with the new name-based ones, but by using your existing list with a separate one-off script you can tell Terraform how to translate these by running a command like this for each entry in the list:
terraform state mv 'aws_iam_user.user[1]' 'aws_iam_user.user["john"]'
After that, Terraform will track these objects by name and thus adding and removing names will affect only the objects relating to the names you changed.
If you aren't ready to do a full switch to for_each right now, you can use a similar strategy with a one-off script to "heal" the hole created by removing an item from your list:
# First, Terraform must "forget" the user that you removed
terraform state rm 'aws_iam_user.user[0]'
# Then renumber the subsequent items to correlate with their new
# positions in the list.
terraform state mv 'aws_iam_user.user[1]' 'aws_iam_user.user[0]'
terraform state mv 'aws_iam_user.user[2]' 'aws_iam_user.user[1]'
# etc, etc
This will of course be a rather tedious, error-prone process to do manually if you have more than a small number of users, so better to write a small program to generate the script.

Resources