테라폼

4-4. 테라폼 - IAM Role과 Policy 구현하기

pininini 2026. 5. 12. 14:53

테라폼 - IAM Role과 Policy 구현하기

AWS 리소스가 어떤 권한으로 동작할지 Terraform으로 정의하기


이전 글에서는 Security Group을 통해 AWS 리소스 간 네트워크 접근 관계를 정의하는 방법을 정리했다.

이번 글에서는 AWS 권한 관리의 핵심인 IAM RolePolicy를 Terraform으로 구현해보려 한다.

AWS를 사용하다 보면 다음과 같은 상황을 자주 만나게 된다.

EC2가 S3에 접근해야 한다.
Lambda가 CloudWatch Logs에 로그를 남겨야 한다.
ECS Task가 ECR에서 이미지를 pull 해야 한다.
애플리케이션이 Secrets Manager 값을 읽어야 한다.
GitHub Actions가 Terraform apply를 실행해야 한다.

이때 필요한 것이 IAM이다.

IAM은 AWS에서 “누가 무엇을 할 수 있는가”를 정의하는 권한 시스템이다.

목차

  • 1. IAM이란 무엇인가
  • 2. IAM User, Role, Policy 차이
  • 3. Trust Policy와 Permission Policy
  • 4. Terraform에서 IAM 작성 방식
  • 5. 기본 예제: EC2 Role 만들기
  • 6. EC2 Instance Profile
  • 7. Lambda Role 만들기
  • 8. ECS Task Execution Role과 Task Role
  • 9. Managed Policy와 Inline Policy
  • 10. data aws_iam_policy_document 사용하기
  • 11. IAM 의존성 흐름
  • 12. 자주 하는 실수
  • 13. 마무리

1. IAM이란 무엇인가

IAM은 Identity and Access Management의 약자다.

AWS에서 사용자, 애플리케이션, 리소스가 어떤 작업을 할 수 있는지 제어하는 서비스다.

예를 들어 다음과 같은 권한을 정의할 수 있다.

EC2가 S3 객체를 읽을 수 있다.
Lambda가 CloudWatch Logs에 로그를 쓸 수 있다.
ECS Task가 Secrets Manager에서 Secret을 읽을 수 있다.
GitHub Actions가 Terraform을 실행할 수 있다.

여기서 중요한 질문은 두 가지다.

1. 누가 이 권한을 사용할 수 있는가?
2. 이 권한으로 무엇을 할 수 있는가?

IAM을 제대로 이해하려면 이 두 질문을 분리해서 봐야 한다.

IAM은 “누가 사용할 수 있는가”와 “무엇을 할 수 있는가”를 분리해서 설계해야 한다.

2. IAM User, Role, Policy 차이

IAM을 처음 접하면 User, Role, Policy가 헷갈릴 수 있다.

간단히 정리하면 다음과 같다.

구분 의미 예시
IAM User 사람 또는 장기 자격 증명을 가진 사용자 관리자 계정, 개발자 계정
IAM Role 특정 주체가 임시로 사용할 수 있는 권한 묶음 EC2 Role, Lambda Role, ECS Task Role
IAM Policy 허용하거나 거부할 작업을 정의한 문서 S3 읽기, CloudWatch Logs 쓰기

실무에서는 가능하면 IAM User의 Access Key를 직접 사용하는 것보다 Role을 사용하는 것이 좋다.

예를 들어 EC2 안에 Access Key를 넣는 방식은 권장하지 않는다.

# 권장하지 않음
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

대신 EC2에 IAM Role을 붙이면 EC2가 해당 Role의 권한으로 AWS API를 호출할 수 있다.

EC2
→ IAM Instance Profile
→ IAM Role
→ Permission Policy

즉, 애플리케이션 코드에 Access Key를 직접 넣지 않아도 된다.


3. Trust Policy와 Permission Policy

IAM Role을 이해할 때 가장 중요한 개념은 두 가지다.

Trust Policy
Permission Policy

이 둘을 구분하지 못하면 IAM이 매우 헷갈린다.


3.1 Trust Policy

Trust Policy는 누가 이 Role을 사용할 수 있는가를 정의한다.

예를 들어 EC2가 사용할 Role이라면 다음처럼 작성한다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

이 의미는 다음과 같다.

EC2 서비스가 이 Role을 Assume 할 수 있다.

Terraform에서는 assume_role_policy에 작성한다.

resource "aws_iam_role" "ec2" {
  name = "demo-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

3.2 Permission Policy

Permission Policy는 이 Role이 무엇을 할 수 있는가를 정의한다.

예를 들어 S3 객체 읽기 권한은 다음처럼 작성할 수 있다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

이 의미는 다음과 같다.

특정 S3 Bucket 안의 객체를 읽을 수 있다.

정리하면 다음과 같다.

구분 질문 예시
Trust Policy 누가 Role을 사용할 수 있는가? EC2, Lambda, ECS Task
Permission Policy Role이 무엇을 할 수 있는가? S3 읽기, 로그 쓰기, Secret 읽기
Trust Policy는 “누가 사용할 수 있는가”, Permission Policy는 “무엇을 할 수 있는가”를 정의한다.

4. Terraform에서 IAM 작성 방식

Terraform에서 IAM을 작성할 때 자주 사용하는 리소스는 다음과 같다.

aws_iam_role
aws_iam_policy
aws_iam_role_policy
aws_iam_role_policy_attachment
aws_iam_instance_profile
리소스 역할
aws_iam_role Role 생성
aws_iam_policy 재사용 가능한 Policy 생성
aws_iam_role_policy Role에 inline policy 직접 추가
aws_iam_role_policy_attachment Role에 managed policy 연결
aws_iam_instance_profile EC2에 Role을 연결하기 위한 프로필

기본 흐름은 다음과 같다.

1. IAM Role 생성
2. Trust Policy 설정
3. Permission Policy 생성
4. Role에 Policy 연결
5. 필요한 리소스에 Role 연결

5. 기본 예제: EC2 Role 만들기

먼저 EC2가 S3 객체를 읽을 수 있는 Role을 만들어보자.

목표는 다음과 같다.

EC2가 IAM Role을 사용한다.
EC2는 특정 S3 Bucket의 객체를 읽을 수 있다.
Access Key는 EC2 내부에 저장하지 않는다.

5.1 variables.tf

variable "project_name" {
  description = "Project name"
  type        = string
}

variable "bucket_name" {
  description = "S3 bucket name to allow read access"
  type        = string
}

5.2 IAM Role

resource "aws_iam_role" "ec2" {
  name = "${var.project_name}-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2-role"
  })
}

이 Role은 EC2가 사용할 수 있다.

그 이유는 Trust Policy에 다음 Principal이 있기 때문이다.

Service = "ec2.amazonaws.com"

5.3 IAM Policy

이제 EC2 Role이 사용할 권한을 만든다.

resource "aws_iam_policy" "s3_read" {
  name        = "${var.project_name}-s3-read-policy"
  description = "Allow read access to specific S3 bucket"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject"
        ]
        Resource = "arn:aws:s3:::${var.bucket_name}/*"
      },
      {
        Effect = "Allow"
        Action = [
          "s3:ListBucket"
        ]
        Resource = "arn:aws:s3:::${var.bucket_name}"
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-s3-read-policy"
  })
}

여기서 s3:GetObjects3:ListBucket은 Resource 범위가 다르다.

s3:GetObject
→ arn:aws:s3:::bucket-name/*

s3:ListBucket
→ arn:aws:s3:::bucket-name

S3 권한을 작성할 때 이 차이를 자주 실수한다.


5.4 Role에 Policy 연결

Policy를 만들었다고 해서 Role이 자동으로 그 권한을 가지는 것은 아니다.

Role에 Policy를 attach 해야 한다.

resource "aws_iam_role_policy_attachment" "ec2_s3_read" {
  role       = aws_iam_role.ec2.name
  policy_arn = aws_iam_policy.s3_read.arn
}

이제 EC2 Role은 해당 S3 읽기 권한을 가진다.

IAM Role
→ IAM Policy attachment
→ S3 읽기 권한

6. EC2 Instance Profile

EC2에 IAM Role을 붙이려면 한 가지 리소스가 더 필요하다.

aws_iam_instance_profile

EC2는 Role을 직접 붙이는 것이 아니라 Instance Profile을 통해 Role을 사용한다.

EC2
→ Instance Profile
→ IAM Role
→ Permission Policy

Terraform 코드는 다음과 같다.

resource "aws_iam_instance_profile" "ec2" {
  name = "${var.project_name}-ec2-profile"
  role = aws_iam_role.ec2.name
}

그리고 EC2 리소스에서 다음처럼 사용한다.

resource "aws_instance" "app" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]

  iam_instance_profile = aws_iam_instance_profile.ec2.name

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2"
  })
}

이제 EC2 내부 애플리케이션은 Access Key 없이도 Role 권한으로 AWS API를 호출할 수 있다.

EC2에서 AWS API를 호출해야 한다면 Access Key를 넣지 말고 IAM Role을 붙이는 것이 좋다.

7. Lambda Role 만들기

Lambda도 실행되기 위해 IAM Role이 필요하다.

Lambda Role의 Trust Policy는 EC2와 다르다.

Service = "lambda.amazonaws.com"

Lambda가 사용할 Role은 다음처럼 만든다.

resource "aws_iam_role" "lambda" {
  name = "${var.project_name}-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-lambda-role"
  })
}

Lambda가 CloudWatch Logs에 로그를 남기려면 기본 로그 권한이 필요하다.

간단한 예제에서는 AWS Managed Policy를 붙일 수 있다.

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

그리고 Lambda 리소스에서 Role ARN을 사용한다.

resource "aws_lambda_function" "app" {
  function_name = "${var.project_name}-lambda"
  role          = aws_iam_role.lambda.arn

  handler = "index.handler"
  runtime = "nodejs20.x"

  filename = "lambda.zip"
}

정리하면 Lambda Role은 다음 흐름으로 이해하면 된다.

Lambda
→ IAM Role
→ CloudWatch Logs 권한
→ 필요한 AWS API 권한

8. ECS Task Execution Role과 Task Role

ECS를 사용할 때는 Role이 두 개 나오는 경우가 많다.

ECS Task Execution Role
ECS Task Role

처음에는 매우 헷갈릴 수 있다.

둘의 차이는 다음과 같다.

구분 역할 예시
Task Execution Role ECS가 Task를 실행하기 위해 사용하는 Role ECR 이미지 pull, CloudWatch Logs 쓰기
Task Role 컨테이너 안의 애플리케이션이 사용하는 Role S3 접근, Secrets Manager 읽기, SQS 호출
Execution Role은 ECS가 쓰는 권한이고, Task Role은 애플리케이션이 쓰는 권한이다.

8.1 ECS Task Execution Role

ECS Task Execution Role의 Trust Policy는 다음 Principal을 사용한다.

Service = "ecs-tasks.amazonaws.com"
resource "aws_iam_role" "ecs_task_execution" {
  name = "${var.project_name}-ecs-task-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ecs-task-execution-role"
  })
}

그리고 ECS Task 실행에 필요한 AWS Managed Policy를 붙일 수 있다.

resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

이 Role은 주로 다음 작업에 사용된다.

ECR에서 이미지 pull
CloudWatch Logs에 로그 전송
일부 Secret/Parameter 주입

8.2 ECS Task Role

Task Role은 컨테이너 안에서 실행되는 애플리케이션이 사용하는 Role이다.

예를 들어 애플리케이션이 S3에 접근해야 한다면 Task Role에 S3 권한을 부여한다.

resource "aws_iam_role" "ecs_task" {
  name = "${var.project_name}-ecs-task-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ecs-task-role"
  })
}

그리고 필요한 권한을 붙인다.

resource "aws_iam_policy" "ecs_task_s3_read" {
  name = "${var.project_name}-ecs-task-s3-read-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject"
        ]
        Resource = "arn:aws:s3:::${var.bucket_name}/*"
      }
    ]
  })
}

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

Task Definition에서는 두 Role을 구분해서 넣는다.

resource "aws_ecs_task_definition" "app" {
  family                   = "${var.project_name}-app"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512

  execution_role_arn = aws_iam_role.ecs_task_execution.arn
  task_role_arn      = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = "app"
      image = "nginx:latest"
    }
  ])
}

이 차이를 이해하면 ECS 권한 설계가 훨씬 쉬워진다.


9. Managed Policy와 Inline Policy

IAM Policy를 Role에 부여하는 방식은 크게 두 가지로 볼 수 있다.

Managed Policy
Inline Policy

9.1 Managed Policy

Managed Policy는 독립적인 Policy 리소스로 만들고, Role에 attach해서 사용한다.

resource "aws_iam_policy" "s3_read" {
  name = "${var.project_name}-s3-read-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = ["s3:GetObject"]
        Resource = "arn:aws:s3:::${var.bucket_name}/*"
      }
    ]
  })
}

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

여러 Role에서 재사용할 수 있고, 권한을 독립적으로 관리하기 쉽다.


9.2 Inline Policy

Inline Policy는 특정 Role에 직접 붙는 Policy다.

resource "aws_iam_role_policy" "ec2_s3_read_inline" {
  name = "${var.project_name}-ec2-s3-read-inline"
  role = aws_iam_role.ec2.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = ["s3:GetObject"]
        Resource = "arn:aws:s3:::${var.bucket_name}/*"
      }
    ]
  })
}

Inline Policy는 해당 Role에 강하게 종속된다.

간단한 권한을 특정 Role에만 붙이고 싶을 때 사용할 수 있다.

구분 Managed Policy Inline Policy
재사용 가능 어려움
관리 단위 Policy 리소스 Role 내부
사용 예 공통 권한 특정 Role 전용 권한

초보 단계에서는 Managed Policy 방식부터 익히는 것이 좋다.


10. data aws_iam_policy_document 사용하기

지금까지는 jsonencode로 Policy를 작성했다.

Terraform에서는 aws_iam_policy_document data source를 사용해서 Policy 문서를 만들 수도 있다.

예를 들어 EC2 Trust Policy를 다음처럼 작성할 수 있다.

data "aws_iam_policy_document" "ec2_assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ec2" {
  name               = "${var.project_name}-ec2-role"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}

S3 읽기 정책도 다음처럼 작성할 수 있다.

data "aws_iam_policy_document" "s3_read" {
  statement {
    effect = "Allow"

    actions = [
      "s3:GetObject"
    ]

    resources = [
      "arn:aws:s3:::${var.bucket_name}/*"
    ]
  }

  statement {
    effect = "Allow"

    actions = [
      "s3:ListBucket"
    ]

    resources = [
      "arn:aws:s3:::${var.bucket_name}"
    ]
  }
}

resource "aws_iam_policy" "s3_read" {
  name   = "${var.project_name}-s3-read-policy"
  policy = data.aws_iam_policy_document.s3_read.json
}

이 방식은 JSON을 직접 작성하는 것보다 Terraform 문법에 가깝고, Policy가 길어질수록 가독성이 좋아질 수 있다.

다만 초보자라면 처음에는 jsonencode 방식으로 IAM Policy 구조를 이해한 뒤, 나중에 aws_iam_policy_document로 넘어가도 충분하다.


11. IAM 의존성 흐름

IAM Role과 Policy의 의존성 흐름을 정리하면 다음과 같다.

IAM Role
├── Trust Policy
├── Permission Policy
└── Policy Attachment

EC2의 경우 Instance Profile이 추가된다.

Permission Policy
→ IAM Role
→ Instance Profile
→ EC2

Lambda의 경우 Role ARN을 직접 사용한다.

Permission Policy
→ IAM Role
→ Lambda

 

ECS의 경우 Role이 두 종류로 나뉜다.

Task Execution Role
→ ECS가 Task 실행에 사용

Task Role
→ 컨테이너 애플리케이션이 사용

 

다음은 EC2의 경우이다.

 

 

중요한 점은 Role 자체만 만든다고 권한이 완성되는 것이 아니라는 점이다.

Role 생성
→ Trust Policy 필요
→ Permission Policy 필요
→ 대상 리소스에 연결 필요

12. 자주 하는 실수

12.1 Trust Policy와 Permission Policy를 헷갈림

Trust Policy는 누가 Role을 사용할 수 있는지를 정의한다.

Permission Policy는 Role이 무엇을 할 수 있는지를 정의한다.

Trust Policy
→ 누가 AssumeRole 할 수 있는가

Permission Policy
→ Assume한 뒤 무엇을 할 수 있는가

이 둘은 반드시 구분해야 한다.


12.2 EC2에 Role만 만들고 Instance Profile을 만들지 않음

EC2는 IAM Role을 직접 붙이지 않고 Instance Profile을 통해 사용한다.

따라서 EC2용 Role을 만들었다면 Instance Profile도 만들어야 한다.

aws_iam_role
→ aws_iam_instance_profile
→ aws_instance

12.3 Access Key를 EC2나 코드에 직접 넣음

EC2, Lambda, ECS에서 AWS API를 호출해야 한다고 해서 Access Key를 코드에 직접 넣으면 안 된다.

# 권장하지 않음
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

대신 IAM Role을 사용해야 한다.


12.4 Resource를 너무 넓게 줌

다음처럼 모든 리소스에 대한 권한을 주면 위험하다.

Resource = "*"

가능하면 필요한 리소스 ARN만 지정한다.

Resource = "arn:aws:s3:::my-bucket/*"

12.5 Action을 너무 넓게 줌

다음처럼 서비스 전체 권한을 주는 것은 편하지만 위험하다.

Action = "s3:*"

가능하면 필요한 Action만 허용한다.

Action = [
  "s3:GetObject",
  "s3:ListBucket"
]

12.6 ECS Execution Role과 Task Role을 헷갈림

ECS에서 가장 흔한 실수 중 하나다.

Execution Role
→ ECS가 이미지 pull, 로그 전송 등에 사용

Task Role
→ 컨테이너 애플리케이션이 AWS API 호출에 사용

애플리케이션이 S3나 Secrets Manager에 접근해야 한다면 Task Role에 권한을 줘야 한다.


12.7 IAM 변경이 EC2 교체로 이어질 수 있음을 놓침

EC2에 연결된 IAM Instance Profile이나 관련 구성을 변경할 때, 상황에 따라 Terraform이 EC2를 교체하려고 할 수 있다.

EC2가 교체되면 instance id, public IP, private IP 등이 바뀔 수 있다.

따라서 EC2의 private IP를 다른 리소스가 직접 바라보는 구조라면 연결이 깨질 수 있다.

EC2 교체
→ private IP 변경
→ 기존 IP를 바라보던 리소스 연결 실패

이 문제는 이후 EC2 글에서 더 자세히 다룰 예정이다.


12.8 권한 전파 시간을 고려하지 않음

IAM 권한은 변경 직후 모든 곳에 즉시 반영되지 않을 수 있다.

Role이나 Policy를 생성한 직후 바로 Lambda 실행, ECS Task 실행 등을 수행하면 권한 전파 지연으로 실패하는 경우가 있다.

이런 경우 CI/CD에서 짧은 대기나 재시도 로직을 두는 것이 도움이 될 수 있다.


13. 마무리

이번 글에서는 Terraform으로 IAM Role과 Policy를 구현하는 방법을 정리했다.

핵심은 다음과 같다.

IAM Role은 권한을 담는 그릇이다.
Trust Policy는 누가 Role을 사용할 수 있는지 정의한다.
Permission Policy는 Role이 무엇을 할 수 있는지 정의한다.
EC2는 Instance Profile을 통해 Role을 사용한다.
Lambda는 Role ARN을 직접 사용한다.
ECS는 Execution Role과 Task Role을 구분해야 한다.
Access Key를 코드에 넣지 말고 Role을 사용해야 한다.

IAM은 Terraform에서 가장 중요하면서도 실수하기 쉬운 영역이다.

처음에는 복잡하게 느껴지지만, 다음 문장만 기억하면 훨씬 이해하기 쉬워진다.

누가 사용할 수 있는가?
무엇을 할 수 있는가?

한 줄 정리

IAM은 “누가 사용할 수 있는가”와 “무엇을 할 수 있는가”를 분리해서 설계해야 한다.


다음 글에서는 EC2를 Terraform으로 구현해본다. EC2는 단순 서버처럼 보이지만 Subnet, Security Group, IAM Role, Storage가 함께 연결되는 리소스다.