Reusable AWS iam role for service-accounts (IRSA for k8s ) terraform module
Tags: aws, terraform, kubernetes
Published: 2022-02-19 06:23:54

AWS supports authenticating your pods using an identity provider that your account is configured to trust.

This tutorial will guide you through the process of creating an IAM role that your kubernetes pods will be able to assume.

Pre-requisites:

- Familiarity with oidc provider in AWS
- Have an oidc provider setup in AWS
- Have a kubernetes cluster running in AWS (of course!)

Creating oidc provider for AWS is beyond the scope of this tutorial.

Now that that's out of the way,
Let's start with some terraform variables:

```terraform
#variables.tf

# this is the url of the oidc_issuer
variable "oidc_issuer" {
  default = "oidc-example-irsa.s3.ap-southeast-2.amazonaws.com/subdomain.example.com"
}
# role arn of the oidc provider
variable "oidc_arn" {
  default = "arn:aws:iam:01234567898:oidc-provider/oidc-example-irsa.s3.ap-southeast-2.amazonaws.com/subdomain.example.com"
}

variable "cluster_name" {
  default = "cluster.example.com"
}

# service account variables
variable "service_accounts" {
  default = [
    {
      name = "blabladefault",
      namespace = "blablans"
      statements = [
        {
          Action   = ["*:*"]
          Effect   = "Deny"
          Resource = "*"
        }
      ]
    }
  ]
}

```
I've prefilled all defaults here because we're going to rely on these defaults. You can also set them to `null` and pass your variables separately.

Next, we'll need a resource block for the iam role.

```terraform
#main.tf
resource "aws_iam_role" "service_accounts" {
  for_each = {for service_account in (var.service_accounts) : service_account.name => service_account }
    name = "${each.value.name}.${each.value.namespace}.sa.${var.cluster_name}"
    
    #allow federated role assumption using webIdentity(oidc)
    assume_role_policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action = "sts:AssumeRoleWithWebIdentity",
          Condition = {
            StringEquals = {
                "${var.oidc_issuer}:sub" = "system:serviceaccount:${each.value.namespace}:${each.value.name}"
            }
          }
          Effect = "Allow"
          Principal = {
            Federated = "${var.oidc_arn}"
          }
        },
      ]
    })

    #generate multiple inline_policy resources 
    dynamic "inline_policy" {
      for_each = "${each.value.statements}"
      content {
        name = "policy-${inline_policy.key}"
        policy = jsonencode({
          Version = "2012-10-17"
          Statement = [{
            Action = inline_policy.value.Action
            Effect = inline_policy.value.Effect
            Resource = inline_policy.value.Resource
          }]
        })
      }
    }

  tags = {
    tag-key = "tag-value"
  }
}
```

#What's happening here?

We're using terraform's [`for_each` meta](https://www.terraform.io/language/meta-arguments/for_each#the-for_each-meta-argument) argument to create multiple iam roles. The resource `aws_iam_role.service_accounts` is thus a `list` of iam roles.

We're then using terraform's [`dynamic` block](https://www.terraform.io/language/expressions/dynamic-blocks) to create multiple `inline_policy` resources within each iam role.

Using a dynamic block inside a `for_each` argument allows us to render nested configurations like above.

The beauty of this type of configuration is that you can reuse this as much as you'd like.

Let's see an example, 

I've updated the `variables.tf` to add one more service account to the service_accounts variable:

```terraform
#variables.tf
# ...
# ...
# [updated] service account variables with two service-accounts
variable "service_accounts" {
  default = [
    {
      name = "blabladefault",
      namespace = "blablans"
      statements = [
        {
          Action   = ["*:*"]
          Effect   = "Deny"
          Resource = "*"
        }
      ]
    },
    {
      name = "blabladefault2",
      namespace = "blablans2"
      statements = [
        {
          Action   = ["*:*"]
          Effect   = "Deny"
          Resource = "*"
        }
      ]
    }
  ]
}
```

Let's run `terraform plan` on this configuration.

We see that it's now creating two service accounts:

```bash
Terraform will perform the following actions:

  # aws_iam_role.service_accounts["blabladefault"] will be created
  + resource "aws_iam_role" "service_accounts" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRoleWithWebIdentity"
                      + Condition = {
                          + StringEquals = {
                              + oidc-example-irsa.s3.ap-southeast-2.amazonaws.com/subdomain.example.com:sub = "system:serviceaccount:blablans:blabladefault"
                            }
                        }
                      + Effect    = "Allow"
                      + Principal = {
                          + Federated = "arn:aws:iam:01234567898:oidc-provider/oidc-example-irsa.s3.ap-southeast-2.amazonaws.com/subdomain.example.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "blabladefault.blablans.sa.cluster.example.com"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags                  = {
          + "tag-key" = "tag-value"
        }
      + tags_all              = {
          + "tag-key" = "tag-value"
        }
      + unique_id             = (known after apply)

      + inline_policy {
          + name   = "policy-0"
          + policy = jsonencode(
                {
                  + Statement = [
                      + {
                          + Action   = [
                              + "*:*",
                            ]
                          + Effect   = "Deny"
                          + Resource = "*"
                        },
                    ]
                  + Version   = "2012-10-17"
                }
            )
        }
    }

  # aws_iam_role.service_accounts["blabladefault2"] will be created
  + resource "aws_iam_role" "service_accounts" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRoleWithWebIdentity"
                      + Condition = {
                          + StringEquals = {
                              + oidc-example-irsa.s3.ap-southeast-2.amazonaws.com/subdomain.example.com:sub = "system:serviceaccount:blablans2:blabladefault2"
                            }
                        }
                      + Effect    = "Allow"
                      + Principal = {
                          + Federated = "arn:aws:iam:01234567898:oidc-provider/oidc-example-irsa.s3.ap-southeast-2.amazonaws.com/subdomain.example.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "blabladefault2.blablans2.sa.cluster.example.com"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags                  = {
          + "tag-key" = "tag-value"
        }
      + tags_all              = {
          + "tag-key" = "tag-value"
        }
      + unique_id             = (known after apply)

      + inline_policy {
          + name   = "policy-0"
          + policy = jsonencode(
                {
                  + Statement = [
                      + {
                          + Action   = [
                              + "*:*",
                            ]
                          + Effect   = "Deny"
                          + Resource = "*"
                        },
                    ]
                  + Version   = "2012-10-17"
                }
            )
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.
```

This plan was generated from the example above. If it doesn't work for you, please leave a comment and I'll try and help you out.