DEV Community

Cover image for AWS IAM roles in Terraform
Dennis Groß (he/him)
Dennis Groß (he/him)

Posted on

AWS IAM roles in Terraform

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

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

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.

resource "aws_iam_role_policy_attachment" "smm_policy_attachment" {
  role       = aws_iam_role.instance_profile.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

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.

Top comments (0)