Library / SDK
terraform-aws-modules/terraform-aws-iam avatar
terraform-aws-modules/terraform-aws-iam

terraform-aws-iam: AWS IAM resources as composable Terraform modules

Terraform module to create AWS IAM resources 🇺🇦

874 stars1,114 forksHCLApache-2.0

At a glance

What is it?
The terraform-aws-iam module set turns AWS IAM into a family of small Terraform modules, one per resource type, that you instantiate separately: an account policy, groups, users, roles, policies, an OIDC provider and a role shaped for EKS service accounts. The design keeps every module focused on a single AWS object, which is why the role module carries four different trust mechanisms rather than one.
Who is it for?
Adopt terraform-aws-iam when you manage AWS IAM across several accounts or repositories and want the trust policy, the permissions and the provider setup written once in a reviewed form rather than copied between state files. Do not adopt it for a single role in a single account, where the module indirection costs more than it saves.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 19 days ago.
What is it written in?
Mainly HCL, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why the module is split the way it is

The module set is organised around one idea: a single AWS resource type per module, instantiated as many times as you need. The README opens with the account module, and the source path in the example tells you the structure:

hcl
module "iam_account" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-account"

  account_alias = "awesome-company"

  max_password_age               = 90
  minimum_password_length        = 24
  require_uppercase_characters   = true
  require_lowercase_characters   = true
  require_numbers                = true
  require_symbols                = true
  password_reuse_prevention      = 3
  allow_users_to_change_password = true
}

The account module creates an account policy and an account alias, and the README notes that module instantiation is once per account. That is the constraint that shapes everything: an account password policy is an account-level singleton, while groups, roles, users and policies are created in numbers.

So the directory layout under modules/ is a catalogue, not a framework. There is a module for the account, one for a group, one for a user, one for a role, one for a policy, one for a read-only policy, one for an OIDC provider and one for a role shaped for EKS service accounts. The examples/ directory mirrors that catalogue with a directory per module, which means every example in the README has a runnable counterpart in the repository.

The advantage of this shape is that a team can depend on the one module it needs and ignore the rest, and that a change to role handling does not force a change to how groups are declared.

The README also points readers at AWS's own published IAM best practices for current guidance, which is the right division of labour. The module encodes the mechanics; the policy decisions about what to grant stay with you.

Groups, policies and the read-only shortcut

The group module creates an IAM group with a policy attached that one or more users can be added to. The example shows the shape, including an inline permissions block and a map of managed policy names to ARNs:

hcl
module "iam_group" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-group"

  name = "superadmins"

  users = [
    "user1",
    "user2"
  ]

  enable_self_management_permissions = true
  permissions = {
    AssumeRole = {
      actions   = ["sts:AssumeRole"]
      resources = ["arn:aws:iam::111111111111:role/admin"]
    }
  }

  policies = {
    AdministratorAccess = "arn:aws:iam::aws:policy/AdministratorAccess",
  }

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

Two things are worth noticing in that block. The permissions argument takes a map of permission blocks with actions and resources, which is a structured form of the inline policy rather than raw JSON. And the policies argument takes a map of names to managed policy ARNs, so the attachment is written as data rather than as a JSON document.

The read-only policy module is the smallest of the set. It creates a policy that allows read-only access to a list of AWS services, and the whole configuration is one argument:

hcl
module "iam_read_only_policy" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-read-only-policy"

  name        = "example"
  path        = "/"
  description = "My example read-only policy"

  allowed_services = ["rds", "dynamo", "health"]

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

The plain policy module, by contrast, takes the policy document as a heredoc. That is the escape hatch: when your statement set is unusual enough that no shorthand fits, you write the JSON yourself and the module handles creation, path, name and tags.

Trust mechanisms: one role module, four ways to establish trust

The role module is the interesting one, because it handles four quite different trust mechanisms behind a single interface.

The base role creates a role with a trust policy and an optional IAM instance profile, described as useful for service roles such as EC2 and ECS, or for roles assumed across AWS accounts. Its trust policy is given as structured blocks with actions, principals and conditions:

hcl
module "iam_role" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role"

  name = "example"

  trust_policy_permissions = {
    TrustRoleAndServiceToAssume = {
      actions = [
        "sts:AssumeRole",
        "sts:TagSession",
      ]
      principals = [{
        type = "AWS"
        identifiers = [
          "arn:aws:iam::835367859851:user/anton",
        ]
      }]
      condition = [{
        test     = "StringEquals"
        variable = "sts:ExternalId"
        values   = ["some-secret-id"]
      }]
    }
  }

  policies = {
    AmazonCognitoReadOnly      = "arn:aws:iam::aws:policy/AmazonCognitoReadOnly"
    AlexaForBusinessFullAccess = "arn:aws:iam::aws:policy/AlexaForBusinessFullAccess"
    custom                     = aws_iam_policy.this.arn
  }

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

The condition block is the part a security reviewer will look at first, because an external ID condition is what stops a confused deputy from using your role. The principals block takes a type and identifiers, which covers both AWS principals and services.

The GitHub OIDC variant flips a single flag, enable_github_oidc, and takes a list of wildcard subjects:

hcl
module "iam_role_github_oidc" {
  source    = "terraform-aws-modules/iam/aws//modules/iam-role"

  enable_github_oidc = true

  # This should be updated to suit your organization, repository, references/branches, etc.
  oidc_wildcard_subjects = ["terraform-aws-modules/terraform-aws-iam:*"]

  policies = {
    S3ReadOnly = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
  }

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

The SAML variant takes enable_saml and a list of saml_provider_ids. The README frames all three as being for trusting external identity providers such as GitHub, Bitbucket, Okta and OneLogin.

The OIDC provider module itself is separate, and carries a constraint worth noting: an IAM provider is one per account per given URL, so the module is provisioned once per AWS account and one or more roles can then be created against it.

The EKS service account role, and the advice to move off it

One module exists specifically for EKS IAM roles for service accounts, IRSA, and it comes with a deprecation note attached.

The README's tip says to upgrade to use EKS Pod Identity instead of IRSA, and points to a separate module for EKS Pod Identity. That recommendation is the most actionable line in the document, because IRSA is a mechanism that AWS has been superseding with a model where the cluster itself brokers credentials.

The module still earns its place, because IRSA roles are a well-trodden path and this one is pre-configured for common EKS add-ons. The example shows the shape:

hcl
module "vpc_cni_irsa" {
  source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts"

  name   = "vpc-cni"

  attach_vpc_cni_policy = true
  vpc_cni_enable_ipv4   = true

  oidc_providers = {
    this = {
      provider_arn               = "arn:aws:iam::012345678901:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/5C54DDF35ER19312844C7333374CC09D"
      namespace_service_accounts = ["kube-system:aws-node"]
    }
  }

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

The oidc_providers argument maps a name to a provider ARN and a list of namespace and service account pairs, so one role can grant permissions to several service accounts. The vpc_cni flags show the pattern of pre-wired add-on policies: you say which add-on you need and the module attaches the right managed policy rather than making you look up the ARN.

If you are starting an EKS project today, the README is telling you to use the other module. If you are maintaining an existing IRSA setup, this one still saves you from hand-writing the trust policy with the service account subject condition.

Users, tags and the known limitations that belong to Terraform

The user module creates an IAM user with the ability to create a login profile, an access key and an SSH key. The example is short:

hcl
module "iam_user" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-user"

  name = "vasya.pupkin"

  force_destroy           = true
  pgp_key                 = "keybase:test"
  password_reset_required = false

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

Two arguments there deserve scrutiny in a real deployment. force_destroy allows Terraform to delete the user even when it has attached resources, which is convenient in a demo and dangerous in an account with anything real on it. The pgp_key setting points at a PGP key source, in the example a keybase entry, for the SSH public key to install.

The tags pattern is consistent across every example in the README: a Terraform marker set to true and an Environment value. That consistency is the module set's way of making it hard to forget tagging, and the Environment key is a convention the terraform-aws-modules organisation applies across its modules.

The README also carries a known limitations section, and it is explicit that the limitations are those of Terraform and OpenTofu rather than of this module. The core one is that Terraform requires lifecycle arguments to be literal values inside the resource block, with a link to the upstream issue. In practice that means count and for_each on a resource inside a module have to be written as literals, which is the classic friction point between module authors and anyone who wants to drive a module with a computed value.

The README's framing of that section, that a few requests come up again and again and cannot be implemented by this module or by any module, is a fair summary of where module boundaries meet language limitations.

The alternatives: plain resources, CloudFormation and the CDK

Two comparisons are worth making, because the module set is not the only way to do this.

The first is writing the IAM resources directly in your own configuration. An aws_iam_role block with an inline trust policy is not long, and a module adds an abstraction layer between you and the AWS API. The difference in approach is repetition. If you have one role in one account, the module is overhead. If you have thirty roles across five accounts and three trust mechanisms, the value is that the trust policy shapes are written once and reviewed once, and the mistakes that come from copying a JSON trust policy into thirty places happen in one place instead of thirty.

The second is CloudFormation or the AWS CDK. CloudFormation's IAM resources are the same AWS objects with a different declaration syntax, and the CDK lets you express them in a programming language with variables, which removes the HCL expression limitations the module's own README complains about. The difference in approach is that the CDK computes the deployment, while Terraform describes the desired state and asks a provider to converge it. For a team already on the CDK, adding a Terraform module set is a second paradigm to maintain, and the CDK's ability to share construct logic between stacks is a genuine advantage over copy-pasting HCL.

A third option is worth naming for anyone who does not want to maintain an access policy at all: an AWS-managed policy set, or a permissions boundary applied centrally. The read-only module in this set is an example of the first, and boundaries are the standard answer to the delegation problem where a team needs to create roles but not grant broad permissions. The module set helps with the mechanics of attaching either, and does not decide for you which permissions a role should hold.

Maintenance, versioning and what to check before you pin

The module set is actively maintained. The last push was on 2026-10-04, and the recent releases are v6.8.2 on 2026-09-18, v6.8.1 on 2026-08-28 and v6.8.0 on 2026-07-28. The six-part version number and the monthly cadence suggest a module set that is versioned independently of the Terraform versions it supports, which is how the terraform-aws-modules organisation generally works: each module has its own release line and its own compatibility matrix.

The licence is Apache-2.0, with the LICENSE file at the root and a CHANGELOG.md alongside it. The .releaserc.json at the root indicates the releases are produced by an automated release process rather than by hand, which is a good sign for consistency but means the changelog is the place to look for what changed in a given patch.

The repository also carries a pre-commit configuration, an editor config and a wrappers/ directory. The wrappers are worth knowing about if you are consuming the module from a mirror or an internal registry, because they are the mechanism by which the module set is republished under different names without editing the source.

What to check before you pin a version is straightforward. Read the changelog for the version you are moving to, since these modules change argument names and defaults between minor versions. Check that the module version you choose supports the Terraform version you run, because the module set tracks Terraform releases. And read the known limitations section again if you plan to use count or for_each with a computed value, because that constraint is the one that most often forces a rewrite of an otherwise working configuration.

Editorial conclusion

Adopt terraform-aws-iam when you manage AWS IAM across several accounts or repositories and want the trust policy, the permissions and the provider setup written once in a reviewed form rather than copied between state files. Do not adopt it for a single role in a single account, where the module indirection costs more than it saves. Verify first that the module version you pin matches your Terraform version, that the trust mechanism you need for the role is the one you configure, and that the known limitations section, which covers Terraform itself rather than this module, does not block the pattern you had in mind.

Frequently asked questions

How is the terraform-aws-iam module set structured?

It is a family of small modules, one per AWS IAM resource type, each instantiated separately: iam-account for the account policy and alias, iam-group, iam-user, iam-role, iam-policy, iam-read-only-policy, iam-oidc-provider and iam-role-for-service-accounts. Each is referenced with a source path such as terraform-aws-modules/iam/aws//modules/iam-group.

Which trust mechanisms does the terraform-aws-iam role module support?

The role module handles four: a plain trust policy with principals and conditions, GitHub OIDC through enable_github_oidc with wildcard subjects, SAML 2.0 through enable_saml and saml_provider_ids, and a variant for EKS IAM roles for service accounts. The README also advises moving from IRSA to EKS Pod Identity, for which a separate module exists.

What limitations does the terraform-aws-iam documentation mention?

The known limitations section is described as covering Terraform and OpenTofu rather than this module. The main one is that Terraform requires lifecycle arguments to be literal values inside the resource block, with an upstream issue linked, which limits computed count and for_each values inside modules.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. README
  4. Releases
  5. terraform-aws-modules/terraform-aws-iam on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/terraform-aws-modules-terraform-aws-iam.svg)](https://hysenlabs.com/projects/terraform-aws-modules-terraform-aws-iam)

Was this useful?