Terraform AWS IAM
Introduction
AWS Identity and Access Management (IAM) is a critical service that helps you control access to your AWS resources. It allows you to manage users, groups, roles, and their permissions within your AWS environment. When combined with Terraform, you can automate the creation and management of these IAM resources, ensuring consistent access controls across your infrastructure.
In this tutorial, we'll explore how to use Terraform to provision and manage AWS IAM resources. We'll cover creating users, groups, roles, and policies, as well as implementing best practices for secure IAM configurations.
Prerequisites
Before you begin, ensure you have:
- Terraform installed (version 1.0.0 or later)
- AWS CLI installed and configured
- Basic understanding of AWS IAM concepts
- Basic knowledge of Terraform
Setting Up the Terraform Project
Let's start by creating a new directory for our Terraform project:
mkdir terraform-aws-iam
cd terraform-aws-iam
touch main.tf variables.tf outputs.tf
Provider Configuration
First, we need to configure the AWS provider in our main.tf
file:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.0.0"
}
provider "aws" {
region = var.aws_region
}
And in variables.tf
:
variable "aws_region" {
description = "The AWS region to deploy resources"
type = string
default = "us-east-1"
}
Managing IAM Users with Terraform
Let's start by creating an IAM user. Add the following to your main.tf
:
resource "aws_iam_user" "developer" {
name = "developer"
path = "/users/"
tags = {
Environment = "Production"
Role = "Developer"
}
}
resource "aws_iam_access_key" "developer_key" {
user = aws_iam_user.developer.name
}
This creates an IAM user named "developer" and generates an access key for this user.
Let's add an output to see the access key information in outputs.tf
:
output "developer_access_key_id" {
value = aws_iam_access_key.developer_key.id
}
output "developer_secret_access_key" {
value = aws_iam_access_key.developer_key.secret
sensitive = true
}
Never commit access keys to version control. The sensitive = true
flag helps prevent the secret from appearing in console output, but additional precautions should be taken in production environments.
Creating IAM Groups
Groups are an effective way to manage permissions for multiple users. Let's create a developers group:
resource "aws_iam_group" "developers" {
name = "developers"
path = "/users/"
}
resource "aws_iam_group_membership" "developers_team" {
name = "developers-group-membership"
users = [aws_iam_user.developer.name]
group = aws_iam_group.developers.name
}
Defining IAM Policies
Now, let's create a policy that grants read-only access to S3 and assign it to our developers group:
resource "aws_iam_policy" "s3_read_only" {
name = "S3ReadOnlyAccess"
description = "Provides read-only access to S3 buckets"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"s3:Get*",
"s3:List*",
"s3:Describe*",
"s3-object-lambda:Get*",
"s3-object-lambda:List*"
],
Resource = "*"
}
]
})
}
resource "aws_iam_group_policy_attachment" "developers_s3_read_only" {
group = aws_iam_group.developers.name
policy_arn = aws_iam_policy.s3_read_only.arn
}
Creating IAM Roles
Roles are typically used by services or for cross-account access. Let's create a role that allows EC2 instances to access S3 buckets:
resource "aws_iam_role" "ec2_s3_access" {
name = "ec2-s3-access-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Service = "ec2.amazonaws.com"
},
Action = "sts:AssumeRole"
}
]
})
tags = {
Purpose = "Allow EC2 instances to access S3"
}
}
resource "aws_iam_role_policy_attachment" "ec2_s3_access" {
role = aws_iam_role.ec2_s3_access.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
resource "aws_iam_instance_profile" "ec2_s3_profile" {
name = "ec2-s3-profile"
role = aws_iam_role.ec2_s3_access.name
}
This creates a role that EC2 instances can assume, and attaches the AWS managed S3 read-only access policy to it.
Implementing IAM Best Practices
Password Policy
Setting a strong password policy is essential for security. Add this to your main.tf
:
resource "aws_iam_account_password_policy" "strict" {
minimum_password_length = 12
require_lowercase_characters = true
require_uppercase_characters = true
require_numbers = true
require_symbols = true
allow_users_to_change_password = true
password_reuse_prevention = 24
max_password_age = 90
}
Multi-Factor Authentication (MFA)
Let's enforce MFA for our developer user:
resource "aws_iam_user_policy" "enforce_mfa" {
name = "enforce-mfa"
user = aws_iam_user.developer.name
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Sid = "AllowViewAccountInfo",
Effect = "Allow",
Action = [
"iam:GetAccountPasswordPolicy",
"iam:GetAccountSummary",
"iam:ListVirtualMFADevices"
],
Resource = "*"
},
{
Sid = "AllowManageOwnVirtualMFADevice",
Effect = "Allow",
Action = [
"iam:CreateVirtualMFADevice",
"iam:DeleteVirtualMFADevice"
],
Resource = "arn:aws:iam::*:mfa/*"
},
{
Sid = "AllowManageOwnUserMFA",
Effect = "Allow",
Action = [
"iam:DeactivateMFADevice",
"iam:EnableMFADevice",
"iam:ListMFADevices",
"iam:ResyncMFADevice"
],
Resource = "arn:aws:iam::*:user/${aws_iam_user.developer.name}"
},
{
Sid = "DenyAllExceptListedIfNoMFA",
Effect = "Deny",
NotAction = [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:ResyncMFADevice",
"sts:GetSessionToken"
],
Resource = "*",
Condition = {
BoolIfExists = {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
})
}
This policy allows the user to manage their own MFA device and denies most actions if MFA is not present.
Real-World Example: Complete IAM Configuration
Let's put everything together into a more comprehensive example for a small organization:
# Define variables for team members
variable "team_members" {
description = "List of team members to create IAM users for"
type = list(string)
default = ["alex", "taylor", "jordan", "casey"]
}
# Create IAM users for each team member
resource "aws_iam_user" "team" {
for_each = toset(var.team_members)
name = each.value
path = "/users/"
tags = {
Team = "Development"
}
}
# Create developer and admin groups
resource "aws_iam_group" "developers" {
name = "developers"
}
resource "aws_iam_group" "administrators" {
name = "administrators"
}
# Add users to groups (first two users are admins, others are developers)
resource "aws_iam_user_group_membership" "user_groups" {
for_each = toset(var.team_members)
user = aws_iam_user.team[each.value].name
groups = [
index(var.team_members, each.value) < 2 ? aws_iam_group.administrators.name : aws_iam_group.developers.name
]
}
# Create custom policy for developers
resource "aws_iam_policy" "developer_permissions" {
name = "DeveloperPermissions"
description = "Permissions for developers"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"ec2:Describe*",
"rds:Describe*",
"s3:List*",
"s3:Get*"
],
Resource = "*"
},
{
Effect = "Allow",
Action = [
"s3:PutObject",
"s3:DeleteObject"
],
Resource = "arn:aws:s3:::dev-*/*"
}
]
})
}
# Attach developer policy to developers group
resource "aws_iam_group_policy_attachment" "developers_permissions" {
group = aws_iam_group.developers.name
policy_arn = aws_iam_policy.developer_permissions.arn
}
# Attach AdministratorAccess policy to administrators group
resource "aws_iam_group_policy_attachment" "admin_permissions" {
group = aws_iam_group.administrators.name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}
# Create a role for Lambda functions
resource "aws_iam_role" "lambda_execution" {
name = "lambda-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Service = "lambda.amazonaws.com"
},
Action = "sts:AssumeRole"
}
]
})
}
# Attach basic Lambda execution policy
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# Add S3 access for Lambda
resource "aws_iam_role_policy" "lambda_s3_access" {
name = "lambda-s3-access"
role = aws_iam_role.lambda_execution.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
Resource = [
"arn:aws:s3:::app-data-bucket",
"arn:aws:s3:::app-data-bucket/*"
]
}
]
})
}
This example:
- Creates users for each team member
- Organizes users into developer and administrator groups
- Assigns appropriate permissions to each group
- Creates a role for Lambda functions with specific S3 access
Visualizing IAM Relationships
Let's create a diagram to help visualize the relationships between IAM resources:
Common IAM Operations in Terraform
Here are some common IAM operations you might need to perform:
Importing Existing IAM Resources
If you already have IAM resources in your AWS account, you can import them into Terraform:
terraform import aws_iam_user.developer "developer"
terraform import aws_iam_group.developers "developers"
terraform import aws_iam_role.ec2_s3_access "ec2-s3-access-role"
Rotating Access Keys
A security best practice is to regularly rotate access keys:
resource "aws_iam_access_key" "developer_key" {
user = aws_iam_user.developer.name
# Set to inactive to create a new key while keeping the old one working
status = "Active"
# Adding a lifecycle block to create a new resource before destroying the old one
lifecycle {
create_before_destroy = true
}
}
Cross-Account Access
For multi-account setups, you might need to allow cross-account access:
resource "aws_iam_role" "cross_account" {
name = "cross-account-access"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
AWS = "arn:aws:iam::ACCOUNT_ID:root"
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)