DEV Community

Arpan
Arpan

Posted on

Reusable AWS iam role for service-accounts (IRSA for k8s ) terraform module

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:

#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 = "*"
        }
      ]
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

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.

#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"
  }
}
Enter fullscreen mode Exit fullscreen mode

What's happening here?

We're using terraform's for_each meta 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 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:

#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 = "*"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Let's run terraform plan on this configuration.

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

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.
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)