For the last few years, my go-to for managing IAM in AWS has been Ansible.  At one of my previous companies, my team had written a really cool Ansible role that not only created the users/groups/roles/policies that we needed, but also compared what was in the account to what was defined in the Ansible and would delete any account that didn’t match.  At my current job, we have some CloudFormation templates that managing some basic IAM bits, but nothing that really manages everything.  While we use Ansible for managing our servers, we do not really use it for managing AWS itself.  For that, we have adopted Terraform.

As you can probably tell from previous posts, I have done a lot of different things with Terraform.  However, managing IAM has not been one of them so I thought I would take a look to see what that would look like.  My first pass was to structure my IAM project the same way that I structure all my Terraform projects.  I checked out my skeleton repo and started a new module for managing IAM.  I generally like using modules as a way to keep my code separate, but as I started to look at the things that I wanted to do it quickly became apparent that using modules was just not going to be the most efficient way to go.  The big problem that I ran in to was the amount of variables and outputs I would have to set to move code back and forth.  If I had separate modules for each IAM type (user, group, role, policy), then I would have to have outputs on most of them because the outputs from one would be variables on another.  It would also introduce problems with dependencies and ordering.

My next thought was to build out one module for all the IAM bits.  That would reduce the amount of outputs and variables I would need since I could call individual resources directly.  As you will see below, I wanted to build each IAM role with a conditional, which means for each role there will be a binary variable.  This caused a lot of unnecessary variables to be created.  They would have to be defined in the variables.tf and main.tf file and instantiated in the environment.tfvars file.  This still seemed like a lot of superfluous work.  But if I don’t put everything in a module, how do I have order in my policies?  You cannot just have Terraform read .tf files from a directory.

New Layout

After thinking on it for a few days, I decided to try a little bit of a different approach.  Since I couldn’t have them divided into different folders I decided to prefix each of my .tf files with a prefix.  I started with my skeleton and then deleted the main.tf, variables.tf and outputs file. Then I added the the prefix 00- so that they show up at the beginning of the list of tf files.  Then I mapped out the prefix numbers for the IAM types, starting with 1000-</code> for users, 2000-</code> for groups, and `5000-</code> for roles.  I did not include a separate prefix for policies since I intend to define them with the group or role that they go with (as explained below). You can find an example repo here.

Initialization

With the exception of the file names, my initialization files remain unchanged.  They are small and I briefly considered combining them into one initialization file, but I still like having them separate.  I’ve also included a Makefile and a Jenkins file to help with running this through automation.

In addition to the -00 initialization files, I have included two additional files.  `01-data-sources.tf </code>currently contains one data source which can be used to return the account number, but I feel I will be able to use it to import currently defined IAM users and roles if I need to.

data "aws_caller_identity" "current" {}

02-password-policy.tf contains a hardened password policy for your AWS account.

resource "aws_iam_account_password_policy" "strict" {
  minimum_password_length = 32
  require_lowercase_characters = true
  require_numbers = true
  require_uppercase_characters = true
  require_symbols = true
  allow_users_to_change_password = true
  password_reuse_prevention = 20
  max_password_age = 180
 }

Users

I debated on whether or not to put users and groups together or keep them separate.  I decided to separate them because we have a lot of users that belong to multiple groups.  We generally have two types of uses.  Ones that go everywhere (global users for automation) and real users that go to our master user account (that can use roles to access other accounts). For each file, I start with a conditional that I can use to set the count on each resource.  Since global users go everywhere, I default to true.

variable "global-users" {default = true}

resource "aws_iam_user" "terraform" {
  count = "${var.global-users}"
  name = "terraform"
  force_destroy = true
}

resource "aws_iam_user_group_membership" "terraform-groups" {
  count = "${var.global-users}"
  user = "${aws_iam_user.terraform.name}"
  groups = [
    "${aws_iam_group.terraform.name}"
  ]
}

This file creates the terraform user and attaches it to the terraform group (that will be shown in the next section).  I am not a fan of attaching policies directly to a user, so I always create a group and attach policies to it.  For the master users account, I default to false since I only want to create it in one account.

variable "create-users" {default = false}

resource "aws_iam_user" "bob-hope" {
  count = "${var.create-users}"
  name = "bob.hope"
  force_destroy = true
}

resource "aws_iam_user_group_membership" "bob-hope-groups" {
  count = "${var.create-users}"
  user = "${aws_iam_user.bob-hope.name}"
  groups = [
    "${aws_iam_group.humans.name}",
    "${aws_iam_group.engineering.name}"
  ]
}

You can add multiple users in this file and attach them to as many groups as you want.

Groups

Since we are attaching users to their respective groups in the user files, creating a group consists of creating the group and attaching a policy.  The following policy creates an engineering group and attaches the AWS defined policy ReadOnlyAccess.

variable "engineering-group" {default = false}

resource "aws_iam_group" "engineering" {
  count = "${var.engineering-group}"
  name = "engineering"
}

resource "aws_iam_group_policy_attachment" "engineering-attach" {
  count = "${var.engineering-group}"
  group = "${aws_iam_group.engineering.name}"
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

I also create custom policies and attach them as well, but they are a little big for this post, but you can find an example here.

Roles

Roles are pretty much done the same way as a group is, by creating the role (and custom policy if necessary) and then attaching policies to the role.  The only addition is the assume_role policy that is needed.

variable "platform-engineering-role" {default = false}

resource "aws_iam_role" "platform-engineering-role" {
  count = "${var.platform-engineering-role}"
  name = "platform-engineering"
  description = "Assume role for Platform Engineers"
  assume_role_policy = "${data.aws_iam_policy_document.platform-engineering-tp.json}"
}

resource "aws_iam_role_policy_attachment" "attach-platform-engineering" {
  count = "${var.platform-engineering-role}"
  role = "${aws_iam_role.platform-engineering-role.name}"
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

data "aws_iam_policy_document" "platform-engineering-tp" {
  statement {
    effect = "Allow"
    actions = [
      "sts:AssumeRole",
    ]
    principals {
      identifiers = ["arn:aws:iam::349863462989:root"]
      type = "AWS"
    }
    condition {
      test = "Bool"
      values = ["true"]
      variable = "aws:MultiFactorAuthPresent"
    }
  }
}

Talk about the projects.

So that’s what I’ve come up with for managing IAM with Terraform.  There are still a few bits that I am not sure I like, but in general I am pretty satisfied with how it turned out.

If you have any ideas on how to improve on this or even a better way to do it, I’d love to hear from you. Drop me an email or leave a comment.