Edge & Routing

AWS IAM

Identity and access management for secure resource control

IAM

Permissions as architecture

What seniors use it for (ML/GenAI)

  • App roles for inference services (ECS/Lambda/SageMaker) to read S3, write logs, call Bedrock, query OpenSearch, etc.
  • Cross-account access (shared data lake, central logging)
  • CI/CD roles (Terraform deployer, build role)
  • Guardrails: permission boundaries, SCPs, separation of duties

Knobs that matter

  • Roles > users for workloads. Humans via SSO/Identity Center; apps via roles.
  • AssumeRole conditions: External ID, source account, aws:PrincipalArn, IP/VPC conditions (where applicable).
  • Session duration: short sessions for humans; workload sessions per service limits.
  • Least privilege: resource-level scoping + conditions (prefix-based S3 access, tag-based access).
  • Permission boundaries (very useful in large orgs): “max possible permissions”.
  • Policy size / sprawl: prefer reusable managed policies for platform patterns, inline for one-off.

Pricing mental model

  • IAM itself is essentially “free”; costs show up indirectly:

    • KMS usage, CloudTrail data events, Access logs, etc.

Heuristics

  • Start with “deny by default” thinking: grant only the 3–5 actions/resources needed.
  • Prefer role-per-workload (not shared) for blast-radius control.
  • Use S3 prefix scoping for multi-tenant buckets: arn:aws:s3:::bucket/prefix/*.

Terraform template (app role + policy for S3 + CloudWatch Logs)

# iam.tf
data "aws_caller_identity" "current" {}

resource "aws_iam_role" "app" {
  name               = "${var.name}-role"
  assume_role_policy = data.aws_iam_policy_document.app_assume.json
  tags               = var.tags
}

# Example: ECS task role (swap principal for lambda/sagemaker/etc)
data "aws_iam_policy_document" "app_assume" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

data "aws_iam_policy_document" "app_policy" {
  statement {
    sid    = "S3ReadWriteScoped"
    effect = "Allow"
    actions = [
      "s3:ListBucket"
    ]
    resources = [var.bucket_arn]
    condition {
      test     = "StringLike"
      variable = "s3:prefix"
      values   = ["${var.bucket_prefix}/*"]
    }
  }

  statement {
    sid    = "S3ObjectAccess"
    effect = "Allow"
    actions = [
      "s3:GetObject", "s3:PutObject", "s3:DeleteObject"
    ]
    resources = ["${var.bucket_arn}/${var.bucket_prefix}/*"]
  }

  statement {
    sid    = "CloudWatchLogs"
    effect = "Allow"
    actions = [
      "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"
    ]
    resources = ["*"]
  }
}

resource "aws_iam_policy" "app" {
  name   = "${var.name}-policy"
  policy = data.aws_iam_policy_document.app_policy.json
  tags   = var.tags
}

resource "aws_iam_role_policy_attachment" "app" {
  role       = aws_iam_role.app.name
  policy_arn = aws_iam_policy.app.arn
}

variable "name"          { type = string }
variable "bucket_arn"     { type = string } # arn:aws:s3:::my-bucket
variable "bucket_prefix"  { type = string } # e.g. "datasets" or "tenantA"
variable "tags"           { type = map(string) default = {} }