AWS IAM roles in Terraform
Tags: terraform, aws, cloud, cloudskills
Published: 2022-12-23 19:00:00

IAM roles can be created with the `aws_iam_role` resource identifier. Other resources such as EC2 instances can assume these roles.

**Content**

## `aws_iam_role` Resource

```jsx
resource "aws_iam_role" "instance_profile" {
  name = "web-server-instance-profile"
  path = "/"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole",
        Principal = {
          Service = "ec2.amazonaws.com"
        },
        Effect = "Allow",
        Sid    = ""
      },
    ]
  })
}
```

The example above contains an IAM role with a role policy that allows the AWS EC2 service to assume another role. the concept of assuming another role through a role is called “role chaining”.

The `aws_iam_role` has the following attributes (non-exhaustive listing)

- `name` (optional) - the name of the role as it will appear in the IAM console
- `path` (optional) - groups roles into a common namespace path (to keep a better overview of your roles)
- `assume_role_policy` (required) - controls what or who can assume this role (principal). This policy is also known under the term “trust policy”
- `managed_policy_arns` (optional) - list of AWS managed policies that you can attach to this role
- `inline_policy` (optional) - provides you the possibility to write a policy that is directly attached to the role.
- `policy` (required) - references a policy document resource that gets attached to the role

## Trust Policies

Every IAM role must specify a trust policy (`assume_role_policy`) as mentioned in the last bullet section.

The trust policy defines who can assume a particular role through the AWS Secure Token Service (STS). The entity that can assume the role is called a principal.

Principals can be

- IAM users
- AWS resources (by ARN)
- Other roles (→ “role chaining”)
- Entire services (e.g. “ec2.amazonaws.com”

## Inline Policies

An inline policy is a policy document that is directly attached to an IAM role and does only exist in the context of this role.

It is a best practice to define IAM policies as dedicated resources if they can be re-used in the context of e.g. other roles. But it is ok to define inline policies if you know that the policy only makes sense in the context of a specific role.

## Managed Policies

AWS provides predefined policy documents for common use cases like using the Cloud Watch Agent in an EC2 instance.

```jsx
resource "aws_iam_role_policy_attachment" "smm_policy_attachment" {
  role       = aws_iam_role.instance_profile.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
```

The code above attaches the managed policy `"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"` to the AWS instance profile role of an EC2 instance. The policy provides basic permissions to the AWS Systems Manager which we need to use the AWS Session Manager to SSH into the EC2 instance.

Using managed policies can be beneficial since you are using best practices provided by AWS. Inn addition to that, managed policies remove maintenance overhead from you since you don’t have to maintain the policy.

On the other hand, managed policies take control away from you since you cannot fine-tune the permissions that they give.

## EC2 Instance Profile

It is a good practice to handle the permissions of an EC2 instance through an IAM role that the instance assumes. This role is called the “instance profile role”.

The EC2 `aws_instance` resource has an `iam_instance_profile` attribute that can be used for this.

```jsx
resource "aws_instance" "web_server" {
  ...
  iam_instance_profile = aws_iam_instance_profile.instance_profile.name
  depends_on = [
    aws_iam_role_policy_attachment.smm_policy_attachment,
    aws_iam_role_policy_attachment.cw_agent_policy_attachment
  ]
	...
}
```

The attribute references an IAM role. In our case, it references a role called `instance_profile` that we create through the `aws_iam_role` resource identifier.

The EC2 instance and IAM role get married together through the `aws_iam_instance_profile` that the `aws_instance` resource references and which on the other hand references the IAM role.

```jsx
resource "aws_iam_instance_profile" "instance_profile" {
  name = "web-server-instance-profile"
  role = aws_iam_role.instance_profile.name
}

resource "aws_iam_role" "instance_profile" {
  name = "web-server-instance-profile"
  path = "/"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole",
        Principal = {
          Service = "ec2.amazonaws.com"
        },
        Effect = "Allow",
        Sid    = ""
      },
    ]
  })
}

resource "aws_iam_policy" "cw_agent_policy" {
  name        = "cw-agent-policy"
  path        = "/"
  description = "policy to alow ec2 instance to push metrics and logs to CloudWatch"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
          "logs:DescribeLogStreams"
        ],
        Effect = "Allow",
        Resource = [
          "*"
        ]
      }
    ]
  })
}
```

This only works if you specify a “trust policy” on the IAM role (`assume_role_policy`). 

We used the “ec2.amazonaws.com” service as a trusted principal in this case, but you could be more restrictive by only allowing the `web_server` EC2 instance as a principal (by the instance ARN).

## `jsonencode`

The `jsonencode` method takes a Terraform object annotation as an argument and converts it to a JSON string that we can use for policy attributes e.g. in the `aws_iam_role` resource.

It is a good practice to use `jsonencode` when you specify policies since it makes it possible to define a JSON String policy while having the usual Linter support for Terraform through your IDE.