Day 16 of my #30DaysOfAWSTerraform challenge focused on one of the most sensitive parts of cloud infrastructure: identity and access management (IAM).Unlike EC2 or networking, IAM mistakes don’t usually “fail loudly” they create security risks. Today’s goal was to learn how to create, manage, and secure multiple IAM users at scale using Terraform.
This day was relatively straightforward conceptually, but the implementation forced me to think like a real administrator rather than a hobbyist.
What I Built
Using Terraform, I automated the creation of:
- Multiple IAM users from a CSV file.
- IAM login profiles with forced password reset.
- Department-based IAM groups.
- Conditional group memberships.
- Custom IAM policies per department.
- Mandatory MFA enforcement for all users.
- Account-wide password policy.
All of this was done declaratively and reproducibly.
Creating Multiple IAM Users from CSV:
Instead of hardcoding users, I used a CSV file as the source of truth. Terraform’s csvdecode() function made this clean and scalable.
locals {
users = csvdecode(file("users.csv"))
}
Each user was created dynamically using for_each:
resource "aws_iam_user" "users" {
for_each = { for user in local.users : user.first_name => user }
name = lower("${substr(each.value.first_name, 0, 1)}${each.value.last_name}")
path = "/users/"
tags = {
DisplayName = "${each.value.first_name} ${each.value.last_name}"
Department = each.value.department
JobTitle = each.value.job_title
Email = each.value.email
Phone = each.value.phone
}
}
This approach allowed me to scale user creation without touching Terraform code again adding a user only requires editing the CSV.
Enabling Console Access and Password Policies:
Each user received a login profile with a forced password reset on first login:
resource "aws_iam_user_login_profile" "users" {
for_each = aws_iam_user.users
user = each.value.name
password_reset_required = true
}
I also enforced a strong account-wide password policy:
resource "aws_iam_account_password_policy" "strong" {
minimum_password_length = 10
require_lowercase_characters = true
require_uppercase_characters = true
allow_users_to_change_password = true
max_password_age = 90
}
This ensures consistency and security across all users.
IAM Groups and Automatic Membership:
I created four IAM groups:
- Management.
- Sales.
- Accounting.
- HR. Membership was determined dynamically using tags instead of manual assignments.
Example Management group membership based on job title:
resource "aws_iam_group_membership" "management_members" {
name = "management-group-membership"
group = aws_iam_group.management.name
users = [
for user in aws_iam_user.users :
user.name
if contains(keys(user.tags), "JobTitle") &&
can(regex("Manager|CEO", user.tags.JobTitle))
]
}
This pattern removes human error and keeps access aligned with organizational roles.
Enforcing MFA for Every User:
One of the most important parts of today was forcing MFA. I created a custom IAM policy that:
- Allows users to set up their own MFA devices.
- Denies all AWS actions if MFA is not enabled.
resource "aws_iam_policy" "force_mfa" {
name = "Force-MFA-Policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DenyAllExceptMFASetupIfNoMFA"
Effect = "Deny"
NotAction = [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"sts:GetSessionToken",
"iam:ChangePassword"
]
Resource = "*"
Condition = {
BoolIfExists = {
"aws:MultifactorAuthPresent" = "false"
}
}
}
]
})
}
This policy was attached to every user automatically:
resource "aws_iam_user_policy_attachment" "enforce_mfa" {
for_each = aws_iam_user.users
user = each.value.name
policy_arn = aws_iam_policy.force_mfa.arn
}
Department-Specific Policies:
Each department received tailored permissions:
- Management: Broad access, restricted IAM management.
- Sales: Read-only access.
- Accounting: Billing and cost management.
- HR: IAM user and credential management only.
These policies were attached at the group level, not the user level a best practice.
Outputs and Visibility:
I exposed useful outputs for visibility and auditing:
output "group_members" {
value = {
Management = length(aws_iam_group_membership.management_members.users)
Sales = length(aws_iam_group_membership.sales_members.users)
Accounting = length(aws_iam_group_membership.accounting_members.users)
HR = length(aws_iam_group_membership.hr_members.users)
}
}
Final Thoughts
Day 16 showed me that IAM is not about permissions it’s about structure. Terraform forces discipline, consistency, and repeatability in access control, which is exactly what real organizations need.
No chaos today just clean, deliberate infrastructure.
On to Day 17.
Top comments (0)