本篇是實作常用的 AWS IAM 服務之 Terraform 模組,並且會使用到 YAML 資料結構來定義模組的內容,完整的專案程式碼分享在我的 Github 上。
./configs/iam/iam.yaml 與五個放置 policy JSON 檔案位置的目錄:
./configs/iam/assume_role_policies: 放置 assume role policy 的目錄./configs/iam/group_policies: 放置 inline group policy 的目錄./configs/iam/policies: 放置 policy 的目錄./configs/iam/role_policies: 放置 inline role policy 的目錄./configs/iam/user_policies: 放置 inline user policy 的目錄my_iam 的放置位置 modules/my_iam:├── configs
│   ├── iam
│   │   ├── assume_role_policies
│   │   ├── group_policies
│   │   ├── policies
│   │   ├── role_policies
│   │   ├── user_policies
│   │   └── iam.yaml
│   ├── subnet
│   └── vpc
├── example.tfvars
├── locals.tf
├── main.tf
├── modules
│   ├── my_iam
│   │   ├── iam_group.tf
│   │   ├── iam_group_policy.tf
│   │   ├── iam_group_policy_attachment.tf
│   │   ├── iam_instance_profile.tf
│   │   ├── iam_policy.tf
│   │   ├── iam_role.tf
│   │   ├── iam_role_policy.tf
│   │   ├── iam_role_policy_attachment.tf
│   │   ├── iam_user.tf
│   │   ├── iam_user_group_membership.tf
│   │   ├── iam_user_policy.tf
│   │   ├── iam_user_policy_attachment.tf
│   │   ├── output.tf
│   │   ├── provider.tf
│   │   └── variables.tf
│   ├── my_igw
│   ├── my_instances
│   ├── my_nacls
│   ├── my_route_tables
│   ├── my_subnets
│   └── my_vpc
└── variables.tf
./configs/iam/iam.yaml 內容來定義 IAM 需要用建立的資源:# Define Policy
policies: []
# Emample: 
# - name: "The name of policy"
#   description: "The description of policy"
#   json_file: ./configs/iam/policies/???????.json
#   path: "/"
# Define user, bind user group and attch existing policy
users: []
# Example:
# - name: "The name of user"
#   groups:
#     - "The name of group"
#   inline_policies:
#     - name: "The name of inline policy"
#     - json_file: ./configs/iam/user_policies/???????.json
#   policy_arns: []
#     - arn:aws:iam::????????????:policy/???????
# Define user group, attch inline and existing policies
groups: []
# Example
# - name: "The name of group"
#   inline_policies:
#     - name: "The name of inline policy"
#     - json_file: ./configs/iam/group_policies/???????.json
#   policy_arns:
#     - arn:aws:iam::????????????:policy/???????
# Define role, attch inline and existing policies
roles: []
# Example
# - name: "The name of role"
#   description: "The description of role"
#   assume_role_policy_json_file: ./configs/iam/assume_role_policies/???????.json
#   inline_policies:
#     - name: "The name of inline policy"
#     - json_file: ./configs/iam/role_policies/???????.json
#   policy_arns: []
#     - arn:aws:iam::????????????:policy/???????
#   tag_name: ""
#   path: "/"
# Define instance profiles
instance_profiles: []
# Example
# - name: "The name of instance profile"
#   role: "The role name of instance profile"
my_iam 模組./modules/my_iam/outputs.tf:output "iam_group_arn" {
  value = aws_iam_group.groups
}
output "iam_role_arn" {
  value = aws_iam_role.roles
}
output "iam_user_arn" {
  value = aws_iam_user.users
}
./modules/my_iam/provider.tf:provider "aws" {
    region  = var.aws_region
    profile = var.aws_profile
}
./modules/my_iam/variables.tf:variable "aws_region" {
  description = "AWS region"
  default     = "ap-northeast-1"
}
variable "aws_profile" {
  description = "AWS profile"
  default     = ""
}
variable "project_name" {
  type    = string
  description = "Project name"
  default = ""
}
variable "department_name" {
  type        = string
  description = "Department name"
  default     = "SRE"
}
variable "iam_path" {
  type    = string
  default = ""
}
./modules/my_iam/iam_group.tf:iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 groups 的 value 為一 list 物件
locals {
  groups = yamldecode(file("${var.iam_path}"))["groups"]
}
resource "aws_iam_group" "groups" {
  for_each = { for r in local.groups : r.name => r }
  name = each.value.name
  path = "/"
}
./modules/my_iam/iam_group_policy.tf:flattern 函式將 group.name 與 inline_policies 轉換成單一列表
locals {
  group_policies = flatten([
    for group in local.groups : [
      for inline_policy in lookup(group, "inline_policies", []) : {
        name      = inline_policy.name
        json_file = inline_policy.json_file
        group     = group.name
      }
    ]
  ])
}
resource "aws_iam_group_policy" "group_policies" {
  for_each = { for r in local.group_policies : "${r.group}:${r.name}" => r }
  name = each.value.name
  policy = file("${each.value.json_file}")
  group = each.value.group
  depends_on = [
    aws_iam_group.groups
  ]
}
./modules/my_iam/iam_group_policy_attachment.tf:flattern 函式將 group.name 與 policy_arns 轉換成單一列表
locals {
  group_policy_association_list = flatten([
    for group in local.groups : [
      for policy_arn in lookup(group, "policy_arns", []) : {
        group      = group.name
        policy_arn = policy_arn
      }
    ]
  ])
}
resource "aws_iam_group_policy_attachment" "attachments" {
  for_each = { for r in local.group_policy_association_list : "${r.group}/${r.policy_arn}" => r }
  policy_arn = each.value.policy_arn
  group      = each.value.group
  depends_on = [
    aws_iam_policy.policies,
    aws_iam_group.groups
  ]
}
./modules/my_iam/iam_instance_profile.tf:iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 instance_profiles 的 value 為一 list 物件
locals {
  instance_profiles = yamldecode(file("${var.iam_path}"))["instance_profiles"]
}
resource "aws_iam_instance_profile" "instance_profile" {
  for_each = { for r in local.instance_profiles : r.name => r }
  name = each.value.name
  role = each.value.role
  tags = {
    Department = var.department_name
    Name       = each.value.name
    Project    = var.project_name
  }
  tags_all = {
    Department = var.department_name
    Name       = each.value.name
    Project    = var.project_name
  }
}
./modules/my_iam/iam_policy.tf:iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 policies 的 value 為一 list 物件
locals {
  policies = yamldecode(file("${var.iam_path}"))["policies"]
}
resource "aws_iam_policy" "policies" {
  for_each = { for r in local.policies : r.name => r }
  description = each.value.description
  name        = each.value.name
  path        = each.value.path
  policy = file("${each.value.json_file}")
  tags = {
    Department = var.department_name
    Project    = var.project_name
  }
  tags_all = {
    Department = var.department_name
    Project    = var.project_name
  }
}
./modules/my_iam/iam_role.tf:iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 roles 的 value 為一 list 物件
locals {
  roles = yamldecode(file("${var.iam_path}"))["roles"]
}
resource "aws_iam_role" "roles" {
  for_each = { for r in local.roles : r.name => r }
  assume_role_policy = each.value.assume_role_policy_json_file != "" ? file("${each.value.assume_role_policy_json_file}") : <<POLICY
  {}
  POLICY
  description          = each.value.description
  max_session_duration = "3600"
  name                 = each.value.name
  path                 = each.value.path
  tags = {
    Department = var.department_name
    Name       = each.value.tag_name
    Project    = var.project_name
  }
  tags_all = {
    Department = var.department_name
    Name       = each.value.tag_name
    Project    = var.project_name
  }
}
./modules/my_iam/iam_role_policy.tf:flattern 函式將 role.name 與 inline_policies 轉換成單一列表
locals {
  role_policies = flatten([
    for role in local.roles : [
      for inline_policy in lookup(role, "inline_policies", []) : {
        name      = inline_policy.name
        json_file = inline_policy.json_file
        role      = role.name
      }
    ]
  ])
}
resource "aws_iam_role_policy" "role_policies" {
  for_each = { for r in local.role_policies : "${r.role}:${r.name}" => r }
  name = each.value.name
  policy = file("${each.value.json_file}")
  role = each.value.role
  depends_on = [
    aws_iam_role.roles
  ]
}
./modules/my_iam/iam_role_policy_attachment.tf:flattern 函式將 role.name 與 policy_arns 轉換成單一列表
locals {
  role_policy_association_list = flatten([
    for role in local.roles : [
      for policy_arn in lookup(role, "policy_arns", []) : {
        role       = role.name
        policy_arn = policy_arn
      }
    ]
  ])
}
resource "aws_iam_role_policy_attachment" "attachments" {
  for_each = { for r in local.role_policy_association_list : "${r.role}/${r.policy_arn}" => r }
  policy_arn = each.value.policy_arn
  role       = each.value.role
  depends_on = [
    aws_iam_policy.policies,
    aws_iam_role.roles
  ]
}
./modules/my_iam/iam_user.tf:iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 users 的 value 為一 list 物件
locals {
  users = yamldecode(file("${var.iam_path}"))["users"]
}
resource "aws_iam_user" "users" {
  # checkov:skip=  CKV_AWS_273: need to do it later
  for_each = { for r in local.users : r.name => r }
  name = each.value.name
  path = "/"
  tags = {
    Department = var.department_name
    Project    = var.project_name
  }
  tags_all = {
    Department = var.department_name
    Project    = var.project_name
  }
}
./modules/my_iam/iam_user_group_membership.tf:iam_path 為傳入的 ./configs/iam/iam.yaml 設定檔路徑位址,透過 yamldecode 取出 key 值 users 的 value 為一 list 物件
locals {
  user_groups = yamldecode(file("${var.iam_path}"))["users"]
}
resource "aws_iam_user_group_membership" "user_groups" {
  for_each = { for r in local.user_groups : r.name => r if length(r.groups) > 0 }
  groups = each.value.groups
  user   = each.value.name
  depends_on = [
    aws_iam_user.users
  ]
}
./modules/my_iam/iam_user_policy.tf:flattern 函式將 user.name 與 inline_polices 轉換成單一列表
locals {
  user_policies = flatten([
    for user in local.users : [
      for inline_policy in lookup(user, "inline_policies", []) : {
        name      = inline_policy.name
        json_file = inline_policy.json_file
        user      = user.name
      }
    ]
  ])
}
resource "aws_iam_user_policy" "user_policies" {
  for_each = { for r in local.user_policies : "${r.user}:${r.name}" => r }
  name = each.value.name
  policy = file("${each.value.json_file}")
  user = each.value.user
  depends_on = [
    aws_iam_user.users
  ]
}
./modules/my_iam/iam_user_policy_attachment.tf:flattern 函式將 user.name 與 policy_arns 轉換成單一列表
locals {
  user_policy_association_list = flatten([
    for user in local.users : [
      for policy_arn in lookup(user, "policy_arns", []) : {
        user       = user.name
        policy_arn = policy_arn
      }
    ]
  ])
}
resource "aws_iam_user_policy_attachment" "attachments" {
  for_each = { for r in local.user_policy_association_list : "${r.user}/${r.policy_arn}" => r }
  policy_arn = each.value.policy_arn
  user       = each.value.user
  depends_on = [
    aws_iam_policy.policies,
    aws_iam_user.users
  ]
}
example.tfvars:aws_region="ap-northeast-1"
aws_profile="<YOUR_PROFILE>"
project_name="example"
department_name="SRE"
main.tf:terraform {
  required_providers {
    aws = {
      version = "5.15.0"
    }
  }
  backend "s3" {
    bucket                  = "<YOUR_S3_BUCKET_NAME>"
    dynamodb_table          = "<YOUR_DYNAMODB_TABLE_NAME>"
    key                     = "terraform.tfstate"
    region                  = "ap-northeast-1"
    shared_credentials_file = "~/.aws/config"
    profile                 = "<YOUR_PROFILE>"
  }
}
# 其他模組省略
# iam
module "iam" {
  aws_profile     = var.aws_profile
  aws_region      = var.aws_region
  department_name = var.department_name
  project_name    = var.project_name
  iam_path        = "./configs/iam/iam.yaml"
  source = "./modules/my_iam"
}
# Define Policy
policies: []
# Define user, bind user group and attch existing policy
users:
  - name: admin-user
    groups: []
    inline_policies:
      - name: admin_access
        json_file: "./configs/iam/user_policies/admin_access.json"
    policy_arns: []
# Define user group, attch inline and existing policies
groups: []
# Define role, attch inline and existing policies
roles: []
instance_profiles: []
terraform init && terraform plan --out .plan -var-file=example.tfvars 來確認一下結果:  # module.iam.aws_iam_user.users["admin-user"] will be created
  + resource "aws_iam_user" "users" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "admin-user"
      + path          = "/"
      + tags          = {
          + "Department" = "SRE"
          + "Project"    = "example"
        }
      + tags_all      = {
          + "Department" = "SRE"
          + "Project"    = "example"
        }
      + unique_id     = (known after apply)
    }
  # module.iam.aws_iam_user_policy.user_policies["admin-user:admin_access"] will be created
  + resource "aws_iam_user_policy" "user_policies" {
      + id     = (known after apply)
      + name   = "admin_access"
      + policy = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = "*"
                      + Effect   = "Allow"
                      + Resource = "*"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + user   = "admin-user"
    }
下一篇文章將會展示實作 AWS S3 之 Terraform 模組。