Terrafrom import GCP IAM member from list of users - terraform-provider-gcp

I want to import existing IAM users into terraform. I have created a module in the main.tf that looks like this
module "iam" {
source = "./modules/iam"
project_id = var.project_id
owners = var.owners
}
The variable is defined in the variable file as:
variable "owners" {
type = list(string)
default = [
"user:test1#infarm.com",
"user:test2#infarm.com",
]
}
The module itself contains a resource with a for_each loop as:
resource "google_project_iam_member" "owner" {
role = "roles/owner"
for_each = toset(concat(
var.owners,
))
member = each.value
}
I have found the import statement from the terraform documentation and interpreted it as the following. According to the documentation this last part user:foo#example.com only applies to a single user. How can I adjust the import statement to apply to the variable list of users that I want?
terraform import module.iam.google_project_iam_member.owner "infarm-data roles/owner user:foo#example.com"

I found the answer to above import issue. The solution required two adjustments.
Reference to the specific item in the for_each loop ["user"] - as mentioned by #luk2302
quotes around the module statement to be interpreted correctly by the import (as shown below)
terraform import 'module.iam.google_project_iam_member.owner["user:foo#example.com"]' "infarm-data roles/owner user:foo#example.com"
Hope this is helpful for someone else

Related

Using terraform to create multiple resources based on a set of variables

I have a set of variables in terraform.tfvars:
resource_groups = {
cow = {
name = "Cow"
location = "eastus"
},
horse = {
name = "Horse"
location = "eastus"
},
chicken = {
name = "Chicken"
location = "westus2"
},
}
my main.tf looks like this:
...
module "myapp" {
source = "./modules/myapp"
resource_groups = var.resource_groups
}
variable "resource_groups" {}
...
./modules/myapp.main.tf look like this:
module "resource_group" {
source = "../myapp.resource_group"
resource_groups = var.resource_groups
for_each = {
for key, value in try(var.resource_groups, {}) : key => value
if try(value.reuse, false) == false
}
}
variable "resource_groups" {}
and ../myapp.resource_group looks like this:
resource "azurerm_resource_group" "resource_group" {
name = var.resource_groups.cow.name
location = var.resource_groups.cow.location
}
variable "resource_groups" {}
My hope is that after terraform plan I would see that three new RGs would be set for addition. Infact I do get three new ones, but they all use the name and location of the cow RG, because I specified var.resource_groups.cow.name The problem is I have tried all kinds of different iterators in place of .cow. and I can't get terraform to use the other variables in the terraform.tfvars file. I have tried square brackets, asterisks, and other wildcards. I am stuck.
I am looking to define a resource in one place and then use that to create multiple instances of that resource per the map of variables.
Guidance would be much appreciated.
Thanks.
Bill
For this situation you'll need to decide whether your module represents one resource group or whether it represents multiple resource groups. For a module that only contains one resource anyway that decision is not particularly important, but I assume you're factoring this out into a separate module because there is something more to this than just the single resource group resource, and so you can decide between these two based on what else this module represents: do you want to repeat everything in this module, or just the resource group resource?
If you need the module to represent a single resource group then you should change its input variables to take the data about only a single resource group, and then pass the current resource group's data in your calling module block.
Inside the module:
variable "resource_group" {
type = object({
name = string
location = string
})
}
resource "azurerm_resource_group" "resource_group" {
name = var.resource_group.name
location = var.resource_group.location
}
When calling the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
module "resource_group" {
source = "../myapp.resource_group"
for_each = var.resource_groups
# each.value is the value of the current
# element of var.resource_groups, and
# so it's just a single resource group.
resource_group = each.value
}
With this strategy, you will declare resource instances with the following addresses, showing that the repetition is happening at the level of the whole module rather than the individual resources inside it:
module.resource_group["cow"].azurerm_resource_group.resource_group
module.resource_group["horse"].azurerm_resource_group.resource_group
module.resource_group["chicken"].azurerm_resource_group.resource_group
If you need the module to represent the full set of resource groups then the module would take the full map of resource groups as an input variable instead of using for_each on the module block. The for_each argument will then belong to the nested resource instead.
Inside the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
resource "azurerm_resource_group" "resource_group" {
for_each = var.resource_groups
name = each.value.name
location = each.value.location
}
When calling the module:
variable "resource_groups" {
type = map(
object({
name = string
location = string
})
)
}
module "resource_group" {
source = "../myapp.resource_group"
# NOTE: No for_each here, because we need only
# one instance of this module which will itself
# then contain multiple instances of the resource.
resource_group = var.resource_groups
}
With this strategy, you will declare resource instances with the following addresses, showing that there's only one instance of the module but multiple instances of the resource inside it:
module.resource_group.azurerm_resource_group.resource_group["cow"]
module.resource_group.azurerm_resource_group.resource_group["horse"]
module.resource_group.azurerm_resource_group.resource_group["chicken"]
It's not clear from the information you shared which of these strategies would be more appropriate in your case, because you've described this module as if it is just a thin wrapper around the azurerm_resource_group resource type and therefore it isn't really clear what this module represents, and why it's helpful in comparison to just writing an inline resource "azurerm_resource_group" block in the root module.
When thinking about which of the above designs is most appropriate for your use-case, I'd suggest considering the advice in When to Write a Module in the Terraform documentation. It can be okay to write a module that contains only a single resource block, but that's typically for more complicated resource types where the module hard-codes some local conventions so that they don't need to be re-specified throughout an organization's Terraform configurations.
If you are just passing the values through directly to the resource arguments with no additional transformation and no additional hard-coded settings then that would suggest that this module is not useful, and that it would be simpler to write the resource block inline instead.

Terraform import: invalid index

I'm trying to replace deprecated resource 'azurerm_sql_server' with the 'azurerm_mssql_server' and got an 'invalid index' error in the case.
A simplified demo of the situation (with Terraform v0.14.5 and v1.0.5):
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=2.49.0"
}
}
}
provider "azurerm" {
features {}
}
locals {
prefix = toset(["primary", "secondary"])
}
resource "azurerm_resource_group" "rg" {
name = "rgtest"
location = "Canada Central"
}
resource "random_password" "sql_admin_password" {
length = 16
special = true
number = true
upper = true
lower = true
min_special = 2
min_numeric = 2
min_upper = 2
min_lower = 2
}
resource "azurerm_sql_server" "instance" {
for_each = local.prefix
name = "${each.value}-sqlsvr"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
version = "12.0"
administrator_login = "ssadmin"
administrator_login_password = random_password.sql_admin_password.result
}
locals {
primary_sql_srv = azurerm_sql_server.instance["primary"].name
secondary_sql_srv = azurerm_sql_server.instance["secondary"].name
}
# other TF resources using local.primary_sql_srv and local.secondary_sql_srv
The infrastructure has been deployed and no intention to re-create the database servers so we need to change the resource and import existing servers. According to Terraform document, this can be done with 'terraform state rm' and 'terraform import' command.
So,
Change the configuration script
...
resource "azurerm_mssql_server" "instance" {
...
locals {
primary_sql_srv = azurerm_mssql_server.instance["primary"].name
secondary_sql_srv = azurerm_mssql_server.instance["secondary"].name
}
# other TF resources using local.primary_sql_srv and local.secondary_sql_srv
Remove azurerm_sql_server resource from the state file, both are successful
terraform.exe state rm azurerm_sql_server.instance[`\`"primary`\`"]
terraform.exe state rm azurerm_sql_server.instance[`\`"secondary`\`"]
Import the primary database server
> terraform.exe import azurerm_mssql_server.instance[`\`"primary`\`"] "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rgtest/providers/Microsoft.Sql/servers/primary-sqlsvr"
azurerm_mssql_server.instance["primary"]: Importing from ID "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rgtest/providers/Microsoft.Sql/servers/primary-sqlsvr"...
azurerm_mssql_server.instance["primary"]: Import prepared!
Prepared azurerm_mssql_server for import
azurerm_mssql_server.instance["primary"]: Refreshing state... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rgtest/providers/Microsoft.Sql/servers/primary-sqlsvr]
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.
Current state list
❯ terraform.exe state list
azurerm_mssql_server.instance["primary"]
azurerm_resource_group.rg
random_password.sql_admin_password
Import the secondary database server
> terraform.exe import azurerm_mssql_server.instance[`\`"secondary`\`"] "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rgtest/providers/Microsoft.Sql/servers/secondary-sqlsvr"
azurerm_mssql_server.instance["secondary"]: Importing from ID "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rgtest/providers/Microsoft.Sql/servers/secondary-sqlsvr"...
azurerm_mssql_server.instance["secondary"]: Import prepared!
Prepared azurerm_mssql_server for import
azurerm_mssql_server.instance["secondary"]: Refreshing state... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rgtest/providers/Microsoft.Sql/servers/secondary-sqlsvr]
Error: Invalid index
on C:\Work\Projects\2021\20210812RenameResource\t1env\main.tf line 49, in locals:
49: secondary_sql_srv = azurerm_mssql_server.instance["secondary"].name
|----------------
| azurerm_mssql_server.instance is object with 1 attribute "primary"
The given key does not identify an element in this collection value.
The state refresh for the 2nd import hit the locals block and failed due to no 'secondary' server resource.
So to me, this is a deadlock, I cannot import the 'secondary' server resource because of the refresh error and the refresh error was caused by the lack of the 'secondary' server resource.
Two ways I can think of:
Manually add the 'secondary' server resource to the state file, which is definitely not proper
Remove the 'locals' block which is OK in the demo but lots of changes in real code for dependencies.
Any thoughts, please? Thank you.
This is a bug in terraform import that was introduced in version 0.13. During a terraform import execution, it will attempt to validate the local variables in the config containing the resource namespace against non-existent state. There are basically three workarounds for this:
Downgrade temporarily to Terraform 0.12 where this bug does not exist.
This is really not a great option because the version(s) is stored in the state, and you may be locked out of executing terraform CLI commands against the state synced with a later version.
Manually modify the state to contain the resources.
Also really not a great option because this could corrupt the state and/or cause other obvious issues with malformation.
Temporarily comment out the relevant locals and any code referencing the local variable values.
This is what I always ended up using. You can do a multiline comment in the /* ... */ style around the relevant locals that references the exported resource attributes of the imported resource, and you will also need to do so in any other areas of the config that reference the local variables. You can then uncomment the code once the imports are complete.
I had the same issue with terraform 1.2.8, updated to 1.3.0 and the import was successfull. Looks like this last version resolves the issue ?
edit : As stated in Terraform v1.3 changelog :
terraform import: Better handling of resources or modules that use for_each, and situations where data resources are needed to complete the operation. (#31283)
This description matches my situation 100% (use of for_each, data blocs & modules).

Reference multiple aws_instance in terraform for template output

We want to deploy services into several regions.
Looks like because of the aws provider, we can't just use count or for_each, as the provider can't be interpolated. Thus I need to set this up manually:
resource "aws_instance" "app-us-west-1" {
provider = aws.us-west-1
#other stuff
}
resource "aws_instance" "app-us-east-1" {
provider = aws.us-east-1
#other stuff
}
I would like when running this to create a file which contains all the IPs created (for an ansible inventory).
I was looking at this answer:
https://stackoverflow.com/a/61788089/169252
and trying to adapt it for my case:
resource "local_file" "app-hosts" {
content = templatefile("${path.module}/templates/app_hosts.tpl",
{
hosts = aws_instance[*].public_ip
}
)
filename = "app-hosts.cfg"
}
And then setting up the template accordingly.
But this fails:
Error: Invalid reference
on app.tf line 144, in resource "local_file" "app-hosts":
122: hosts = aws_instance[*].public_ip
A reference to a resource type must be followed by at least one attribute
access, specifying the resource name
I am suspecting that I can't just reference all the aws_instance defined as above like this. Maybe to refer to all aws_instance in this file I need to use a different syntax.
Or maybe I need to use a module somehow. Can someone confirm this?
Using terraform v0.12.24
EDIT: The provider definitions use alias and it's all in the same app.tf, which I was naively assuming to be able to apply in one go with terraform apply (did I mention I am a beginner with terraform?):
provider "aws" {
alias = "us-east-1"
region = "us-east-1"
}
provider "aws" {
alias = "us-west-1"
region = "us-west-1"
}
My current workaround is to not do a join but simply listing them all individually:
{
host1 = aws_instance.app-us-west-1.public_ip
host2 = aws_instance.app-us-east-1.public_ip
# more hosts
}

Issues with importing resource to terraform module

I have created module named workflow for Azure LogicApp
Here is the module:
resource "azurerm_logic_app_workflow" "LogicApp" {
name = "${var.LogicAppName}"
location = "${var.LogicAppLocation}"
resource_group_name = "${var.rgName}"
workflow_schema = "${var.schema}"
}
In workflow_schema i'm specifying the path to my file which contains the logicapp configuration
In main config.tf I have the following setup:
module "workflow" {
source = "./modules/workflow/"
LogicAppName = "LaName"
LogicAppLocation = "${azurerm_resource_group.rg.location}"
rgName = "${azurerm_resource_group.rg.name}"
schema = "${file("./path/to/the/file/LaName")}"
}
So, when I'm running terraform init and terraform plan everything works perfectly fine.
Since my logic app was created earlier, I want to import it so that terraform apply won't overwrite it.
I am running the following command and it returns the error:
terraform import module.workflow.azurerm_logic_app_workflow.LogicApp /subscriptions/mySubscriptionID/resourceGroups/myRgName/providers/Microsoft.Logic/workflows/LaName
Error: Import to non-existent module
module.workflow is not defined in the configuration. Please add configuration
for this module before importing into it.
I'm using the following versions of software:
Terraform v0.12.13
+ provider.azurerm v1.28.0
If anyone has any ideas why terraform import fails, please share them.
I see the issue in naming.
Your module is named workflow and in your configuration you name the resource workflow too, this should be different. You are trying to import into the resource directly.
Example:
module "workflow-azure" {
source = "./modules/workflow/"
LogicAppName = "LaName"
LogicAppLocation = "${azurerm_resource_group.rg.location}"
rgName = "${azurerm_resource_group.rg.name}"
schema = "${file("./path/to/the/file/LaName")}"
}
and the import should be then
terraform import module.workflow-azure.azurerm_logic_app_workflow.LogicApp /subscriptions/mySubscriptionID/resourceGroups/myRgName/providers/Microsoft.Logic/workflows/LaName

How to iterate multiple resources over the same list?

New to Terraform here. I'm trying to create multiple projects (in Google Cloud) using Terraform. The problem is I've to execute multiple resources to completely set up a project. I tried count, but how can I tie multiple resources sequentially using count? Here are the following resources I need to execute per project:
Create project using resource "google_project"
Enable API service using resource "google_project_service"
Attach the service project to a host project using resource "google_compute_shared_vpc_service_project" (I'm using shared VPC)
This works if I want to create a single project. But, if I pass a list of projects as input, how can I execute all the above resources for each project in that list sequentially?
Eg.
Input
project_list=["proj-1","proj-2"]
Execute the following sequentially:
resource "google-project" for "proj-1"
resource "google_project_service" for "proj-1"
resource "google_compute_shared_vpc_service_project" for "proj-1"
resource "google-project" for "proj-2"
resource "google_project_service" for "proj-2"
resource "google_compute_shared_vpc_service_project" for "proj-2"
I'm using Terraform version 0.11 which does not support for loops
In Terraform, you can accomplish this using count and the two interpolation functions, element() and length().
First, you'll give your module an input variable:
variable "project_list" {
type = "list"
}
Then, you'll have something like:
resource "google_project" {
count = "${length(var.project_list)}"
name = "${element(var.project_list, count.index)}"
}
resource "google_project_service" {
count = "${length(var.project_list)}"
name = "${element(var.project_list, count.index)}"
}
resource "google_compute_shared_vpc_service_project" {
count = "${length(var.project_list)}"
name = "${element(var.project_list, count.index)}"
}
And of course you'll have your other configuration in those resource declarations as well.
Note that this pattern is described in Terraform Up and Running, Chapter 5, and there are other examples of using count.index in the docs here.
A small update to this question/answer (terraform 0.13 and above). The count or length is not advisable to use anymore due to the way that terraforms works, let's imagine the next scenario:
Suppose you have an array with 3 elements: project_list=["proj-1","proj-2","proj-3"], once you apply that if you want to delete the "proj-2" item from your array once you run the plan, terraform will modify your second element to "proj-3" instead of removing It from the list (more info in this good post). The solution to get the proper behavior is to use the for_each function as follow:
variable "project_list" {
type = list(string)
}
resource "google_project" {
for_each = toset(var.project_list)
name = each.value
}
resource "google_project_service" {
for_each = toset(var.project_list)
name = each.value
}
resource "google_compute_shared_vpc_service_project" {
for_each = toset(var.project_list)
name = each.value
}
Hope this helps! 👍

Resources