diff --git a/terraform/aws/iam-role/README.md b/terraform/aws/iam-role/README.md
new file mode 100644
index 0000000..734c55a
--- /dev/null
+++ b/terraform/aws/iam-role/README.md
@@ -0,0 +1,52 @@
+# iam-role
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.0 |
+| [aws](#requirement\_aws) | ~> 5.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | 5.100.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_iam_role.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
+| [aws_iam_role_policy.inline](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
+| [aws_iam_role_policy_attachment.managed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [assume\_role\_policy](#input\_assume\_role\_policy) | JSON-formatted assume role policy document (trust policy) | `string` | n/a | yes |
+| [description](#input\_description) | Description of the IAM role | `string` | `"IAM role created by Terraform"` | no |
+| [force\_detach\_policies](#input\_force\_detach\_policies) | Whether to force detach policies when destroying the role | `bool` | `false` | no |
+| [inline\_policies](#input\_inline\_policies) | Map of inline policy names to policy documents (JSON strings) | `map(string)` | `{}` | no |
+| [managed\_policy\_arns](#input\_managed\_policy\_arns) | List of ARNs of managed policies to attach to the role | `list(string)` | `[]` | no |
+| [max\_session\_duration](#input\_max\_session\_duration) | Maximum session duration (in seconds) for the role (3600-43200) | `number` | `3600` | no |
+| [name](#input\_name) | Name of the IAM role | `string` | n/a | yes |
+| [path](#input\_path) | Path for the IAM role | `string` | `"/"` | no |
+| [permissions\_boundary](#input\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the role | `string` | `null` | no |
+| [tags](#input\_tags) | Tags to apply to the IAM role | `map(string)` | `{}` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [role\_arn](#output\_role\_arn) | ARN of the IAM role |
+| [role\_id](#output\_role\_id) | ID of the IAM role |
+| [role\_name](#output\_role\_name) | Name of the IAM role |
+| [role\_unique\_id](#output\_role\_unique\_id) | Unique ID assigned by AWS |
+
diff --git a/terraform/aws/iam-role/main.tf b/terraform/aws/iam-role/main.tf
new file mode 100644
index 0000000..6bed0ae
--- /dev/null
+++ b/terraform/aws/iam-role/main.tf
@@ -0,0 +1,60 @@
+# -----------------------------------------------------------------------------
+# IAM Role Module
+# Creates an IAM role with assume role policy and optional inline/managed policies
+# -----------------------------------------------------------------------------
+
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "~> 5.0"
+ }
+ }
+}
+
+# -----------------------------------------------------------------------------
+# IAM Role
+# -----------------------------------------------------------------------------
+
+resource "aws_iam_role" "this" {
+ name = var.name
+ description = var.description
+ assume_role_policy = var.assume_role_policy
+
+ max_session_duration = var.max_session_duration
+ path = var.path
+ permissions_boundary = var.permissions_boundary
+ force_detach_policies = var.force_detach_policies
+
+ tags = merge(
+ var.tags,
+ {
+ Name = var.name
+ }
+ )
+}
+
+# -----------------------------------------------------------------------------
+# Managed Policy Attachments
+# -----------------------------------------------------------------------------
+
+resource "aws_iam_role_policy_attachment" "managed" {
+ for_each = toset(var.managed_policy_arns)
+
+ role = aws_iam_role.this.name
+ policy_arn = each.value
+}
+
+# -----------------------------------------------------------------------------
+# Inline Policies
+# -----------------------------------------------------------------------------
+
+resource "aws_iam_role_policy" "inline" {
+ for_each = var.inline_policies
+
+ name = each.key
+ role = aws_iam_role.this.id
+ policy = each.value
+}
diff --git a/terraform/aws/iam-role/outputs.tf b/terraform/aws/iam-role/outputs.tf
new file mode 100644
index 0000000..8e57b26
--- /dev/null
+++ b/terraform/aws/iam-role/outputs.tf
@@ -0,0 +1,23 @@
+# -----------------------------------------------------------------------------
+# IAM Role Outputs
+# -----------------------------------------------------------------------------
+
+output "role_arn" {
+ description = "ARN of the IAM role"
+ value = aws_iam_role.this.arn
+}
+
+output "role_name" {
+ description = "Name of the IAM role"
+ value = aws_iam_role.this.name
+}
+
+output "role_id" {
+ description = "ID of the IAM role"
+ value = aws_iam_role.this.id
+}
+
+output "role_unique_id" {
+ description = "Unique ID assigned by AWS"
+ value = aws_iam_role.this.unique_id
+}
diff --git a/terraform/aws/iam-role/variables.tf b/terraform/aws/iam-role/variables.tf
new file mode 100644
index 0000000..edc9012
--- /dev/null
+++ b/terraform/aws/iam-role/variables.tf
@@ -0,0 +1,86 @@
+# -----------------------------------------------------------------------------
+# IAM Role Variables
+# -----------------------------------------------------------------------------
+
+variable "name" {
+ description = "Name of the IAM role"
+ type = string
+
+ validation {
+ condition = can(regex("^[a-zA-Z][a-zA-Z0-9-_+=,.@-]*$", var.name)) && length(var.name) <= 64
+ error_message = "Role name must start with a letter, contain only alphanumeric characters and -_+=,.@- symbols, and be up to 64 characters long."
+ }
+}
+
+variable "description" {
+ description = "Description of the IAM role"
+ type = string
+ default = "IAM role created by Terraform"
+}
+
+variable "assume_role_policy" {
+ description = "JSON-formatted assume role policy document (trust policy)"
+ type = string
+
+ validation {
+ condition = can(jsondecode(var.assume_role_policy))
+ error_message = "The assume role policy must be a valid JSON document."
+ }
+}
+
+variable "managed_policy_arns" {
+ description = "List of ARNs of managed policies to attach to the role"
+ type = list(string)
+ default = []
+}
+
+variable "inline_policies" {
+ description = "Map of inline policy names to policy documents (JSON strings)"
+ type = map(string)
+ default = {}
+
+ validation {
+ condition = alltrue([for policy in values(var.inline_policies) : can(jsondecode(policy))])
+ error_message = "All inline policies must be valid JSON documents."
+ }
+}
+
+variable "max_session_duration" {
+ description = "Maximum session duration (in seconds) for the role (3600-43200)"
+ type = number
+ default = 3600
+
+ validation {
+ condition = var.max_session_duration >= 3600 && var.max_session_duration <= 43200
+ error_message = "Max session duration must be between 3600 (1 hour) and 43200 (12 hours)."
+ }
+}
+
+variable "path" {
+ description = "Path for the IAM role"
+ type = string
+ default = "/"
+
+ validation {
+ condition = can(regex("^/.*/$", var.path)) || var.path == "/"
+ error_message = "Path must begin and end with /."
+ }
+}
+
+variable "permissions_boundary" {
+ description = "ARN of the policy that is used to set the permissions boundary for the role"
+ type = string
+ default = null
+}
+
+variable "force_detach_policies" {
+ description = "Whether to force detach policies when destroying the role"
+ type = bool
+ default = false
+}
+
+variable "tags" {
+ description = "Tags to apply to the IAM role"
+ type = map(string)
+ default = {}
+}