Home > Software engineering >  Reuse imported aws_iam_service_linked_role in module instantiated with for_each
Reuse imported aws_iam_service_linked_role in module instantiated with for_each

Time:08-29

I have a module, let's say 'redshift_dw' that contains the aws_iam_service_linked_role for redshift.amazonaws.com.

The service role already exists in AWS, so I import it:

terraform import aws_iam_service_linked_role.redshift "arn:aws:iam::${AWSACCOUNTID}:role/aws-service-role/redshift.amazonaws.com/AWSServiceRoleForRedshift"

In my config I call the module multiple times (via locals, yamldecode(config.yml) and for_each, similar to this blog post).

My set up:

.
├── environments
│   ├── prod
│   └── test
│       ├── config.yml
│       ├── locals.tf
│       ├── iam.tf
│       ├── modules.tf
│       ├── providers.tf
│       ├── terraform.tf
└── modules
    └── redshift_dw
        ├── iam.tf
        ├── outputs.tf
        ├── providers.tf
        ├── redshift.tf
        └── variables.tf

environment folder

Input values:

# environments/test/config.yml

clients:
  - code: XYZ
    other_var: 123

Reading the input values as locals:

# environments/test/locals.tf

locals {
  config = yamldecode(file("${path.root}/config.yml"))
}

# environments/test/iam.tf

resource "aws_iam_service_linked_role" "redshift" {
  aws_service_name = "redshift.amazonaws.com"
}

Create module instances for each client:

# environments/test/modules.tf

module "my_module_call_for_redshift_dw" {
  for_each = { for x in local.config.clients : x.code => x }
  source = '../../modules/redshift_dw'
  
  some_var = each.value.some_var
  
  # further configuration
}

modules folder

Creating the resource in the module:

# modules/redshift_dw/iam.tf

resource "aws_iam_service_linked_role" "redshift" {
  aws_service_name = var.rs_service_linked_role
}


EDIT: Consuming the linked role in aws_redshift_cluster.iam_roles:

# modules/redshift_dw/redshift.tf

resource "aws_redshift_cluster" "default" {
  cluster_identifier = 'cluster-XYZ'
  # ...
  iam_roles = [
    aws_iam_role.client.arn,
    aws_iam_service_linked_role.redshift.arn  # <---
  ]

/EDIT


Input value as a default value:

# modules/redshift_dw/variables.tf

variable "rs_service_linked_role" {
  type = string
  default = "redshift.amazonaws.com"
}

What happens

If I do a terraform plan now, it wants to create a resource for each module, instead of "re-using" the only one that I initially imported:

Terraform will perform the following actions:

  # module.redshift_dw["XYZ"].aws_iam_service_linked_role.redshift will be created
    resource "aws_iam_service_linked_role" "redshift" {
        arn              = (known after apply)
        aws_service_name = "redshift.amazonaws.com"
        create_date      = (known after apply)
        id               = (known after apply)
        name             = (known after apply)
        path             = (known after apply)
        tags_all         = (known after apply)
        unique_id        = (known after apply)
    }

But I would expect it to READ the existing resource. As I understand I cannot create a service linked role for each instance/client but need to re-use the only existing one. How is that possible?

CodePudding user response:

Based on the requirements defined in the question, I would say you need to either:

  • Use the service linked role IAM resource attribute reference (a better way)
  • Hardcode the IAM service linked role

To achieve that, in the test environment, when calling the Redshift module, you would just specify the attribute exported by the resource as the input for a module variable and remove the resource for the service linked role completely:

# NOT NEEDED in modules/redshift_dw/iam.tf
resource "aws_iam_service_linked_role" "redshift" {
  aws_service_name = var.rs_service_linked_role
}

Based on the fact that iam.tf and Redshift module call are in the same directory, change you would have to make is:

module "my_module_call_for_redshift_dw" {
  for_each = { for x in local.config.clients : x.code => x }
  source = '../../modules/redshift_dw'
  # further configuration
  service_linked_role_arn = aws_iam_service_linked_role.redshift.arn
}

You would also have to have the variable defined on the Redshift module level:

variable "service_linked_role_arn" {
  description = "Service linked role ARN for Redshift."
  type        = string
}

The module code for the Redshift cluster would then look like:

resource "aws_redshift_cluster" "default" {
  cluster_identifier = 'cluster-XYZ'
  # ...
  iam_roles = [
    aws_iam_role.client.arn,
    var.service_linked_role_arn
  ]
  ...
}

Adding resource blocks in modules (and anywhere else for that matter) will always try to create a new resource. In this case, it would fail because that service linked role already exists, but plan shows what it would have tried to do.

Usually, there is another way to get information about what was created already, and those are data sources. However, I do not think there is one for the service linked role. Not entirely sure, but you could maybe fetch the service linked role by using data source [1] if you do not want to update the module code.


[1] https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_role

  • Related