테라폼

4-11. 테라폼 - ECS Fargate 기본 구현하기

pininini 2026. 5. 14. 19:52

테라폼 - ECS Fargate 기본 구현하기

ECR 이미지를 가져와 컨테이너를 실행하고 ALB와 연결하기


이전 글에서는 Terraform으로 ECR을 구현하는 방법을 정리했다.

이번 글에서는 ECR에 저장된 Docker 이미지를 실행하는 ECS Fargate를 Terraform으로 구현해보려 한다.

ECS는 Elastic Container Service의 약자다.

AWS에서 컨테이너를 실행하고 관리하는 서비스이며, Fargate를 사용하면 EC2 서버를 직접 관리하지 않고 컨테이너를 실행할 수 있다.

ECR
→ Docker Image 저장

ECS Fargate
→ Docker Image 실행

ALB
→ 외부 요청을 ECS Task로 전달

ECS Fargate는 처음 보면 리소스가 많아 보여서 어렵게 느껴질 수 있다.

하지만 핵심 구조는 단순하다.

ECS Fargate는 ECR 이미지를 가져와 Task로 실행하고, Service가 원하는 개수만큼 Task를 유지하는 구조다.

목차

  • 1. ECS Fargate란 무엇인가
  • 2. ECS를 구성하는 주요 요소
  • 3. ECS Cluster 만들기
  • 4. Task Execution Role과 Task Role
  • 5. CloudWatch Log Group 만들기
  • 6. Task Definition 만들기
  • 7. ECS Service 만들기
  • 8. ALB Target Group과 연결하기
  • 9. Security Group 구성
  • 10. 환경 변수와 Secret 주입하기
  • 11. desired_count와 배포 관리
  • 12. 실전 예제: ALB → ECS Fargate 구조
  • 13. 의존성 흐름
  • 14. 자주 하는 실수
  • 15. 마무리

1. ECS Fargate란 무엇인가

ECS는 AWS에서 컨테이너를 실행하고 관리하는 서비스다.

컨테이너를 실행하려면 원래 서버가 필요하다.

EC2
→ Docker 설치
→ 컨테이너 실행

하지만 Fargate를 사용하면 EC2 인스턴스를 직접 만들고 관리하지 않아도 된다.

Docker Image
→ ECS Fargate
→ 컨테이너 실행

즉, Fargate는 서버 관리 부담을 줄여주는 컨테이너 실행 방식이다.

ECS에는 크게 두 가지 실행 방식이 있다.

구분 설명 특징
ECS on EC2 EC2 인스턴스 위에서 컨테이너 실행 서버 관리 필요
ECS Fargate 서버 관리 없이 컨테이너 실행 초보자와 운영 단순화에 유리

이 글에서는 Fargate 기준으로 설명한다.


2. ECS를 구성하는 주요 요소

ECS Fargate를 이해하려면 다음 요소를 알아야 한다.

구성 요소 Terraform 리소스 역할
Cluster aws_ecs_cluster ECS 리소스를 묶는 논리적 공간
Task Definition aws_ecs_task_definition 컨테이너 실행 설정
Service aws_ecs_service Task를 원하는 개수만큼 유지
Task Execution Role aws_iam_role ECS가 이미지 pull, 로그 전송에 사용
Task Role aws_iam_role 컨테이너 애플리케이션이 AWS API 호출에 사용
Log Group aws_cloudwatch_log_group 컨테이너 로그 저장
Security Group aws_security_group ALB와 ECS Task 간 접근 제어

구조를 단순화하면 다음과 같다.

ECR Image
→ Task Definition
→ ECS Service
→ ECS Task 실행

외부 요청까지 포함하면 다음 구조가 된다.

User
→ ALB
→ Target Group
→ ECS Service
→ ECS Task

3. ECS Cluster 만들기

ECS Cluster는 ECS 리소스를 묶는 논리적 공간이다.

Fargate에서는 EC2 인스턴스를 직접 Cluster에 등록하지 않아도 된다.

resource "aws_ecs_cluster" "app" {
  name = "${var.project_name}-${var.environment}-cluster"

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

Cluster 자체는 비교적 단순한 리소스다.

실제 컨테이너 실행 설정은 Task Definition과 Service에서 결정된다.


4. Task Execution Role과 Task Role

ECS를 처음 다룰 때 가장 헷갈리는 부분이 Role이다.

ECS에서는 보통 두 종류의 Role을 구분한다.

Task Execution Role
Task Role
Role 사용 주체 사용 예
Task Execution Role ECS / Fargate Agent ECR 이미지 pull, CloudWatch Logs 전송, Secret 주입
Task Role 컨테이너 안의 애플리케이션 S3 접근, Secrets Manager 조회, SQS 호출
Execution Role은 ECS가 쓰는 권한이고, Task Role은 애플리케이션이 쓰는 권한이다.

4.1 Task Execution Role

Task Execution Role은 ECS가 Task를 실행하기 위해 사용하는 Role이다.

resource "aws_iam_role" "ecs_task_execution" {
  name = "${var.project_name}-${var.environment}-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}-${var.environment}-ecs-task-execution-role"
  })
}

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

이 AWS Managed Policy에는 기본적인 ECR pull, CloudWatch Logs 전송 권한이 포함된다.


4.2 Task Role

Task Role은 컨테이너 내부의 애플리케이션이 사용하는 Role이다.

예를 들어 애플리케이션이 S3를 읽거나 Secrets Manager 값을 직접 조회한다면 Task Role에 권한을 부여해야 한다.

resource "aws_iam_role" "ecs_task" {
  name = "${var.project_name}-${var.environment}-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}-${var.environment}-ecs-task-role"
  })
}

처음에는 권한 없이 Role만 만들어두고, 이후 애플리케이션이 필요한 AWS API 권한만 추가해도 된다.

예:
S3 읽기 필요
→ Task Role에 s3:GetObject 추가

Secrets Manager 조회 필요
→ Task Role에 secretsmanager:GetSecretValue 추가

5. CloudWatch Log Group 만들기

ECS 컨테이너 로그는 CloudWatch Logs로 보낼 수 있다.

이를 위해 Log Group을 먼저 만든다.

resource "aws_cloudwatch_log_group" "app" {
  name              = "/ecs/${var.project_name}/${var.environment}/app"
  retention_in_days = 14

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-app-log-group"
  })
}

retention_in_days는 로그 보관 기간이다.

로그를 무제한으로 보관하면 비용이 계속 증가할 수 있으므로, 운영 정책에 맞게 보관 기간을 정하는 것이 좋다.

학습용 / 개발용
→ 7일 또는 14일

운영
→ 30일, 90일, 180일 등 정책에 따라 설정

6. Task Definition 만들기

Task Definition은 컨테이너 실행 설정이다.

어떤 이미지를 사용할지, CPU와 메모리는 얼마인지, 어떤 포트를 열지, 로그는 어디로 보낼지 정의한다.

resource "aws_ecs_task_definition" "app" {
  family                   = "${var.project_name}-${var.environment}-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 = "${var.ecr_repository_url}:${var.image_tag}"

      essential = true

      portMappings = [
        {
          containerPort = 8080
          protocol      = "tcp"
        }
      ]

      environment = [
        {
          name  = "APP_ENV"
          value = var.environment
        }
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.app.name
          awslogs-region        = var.aws_region
          awslogs-stream-prefix = "ecs"
        }
      }
    }
  ])

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-app-task-definition"
  })
}

주요 설정은 다음과 같다.

설정 의미
requires_compatibilities Fargate 사용 여부
network_mode Fargate에서는 awsvpc 사용
cpu / memory Task에 할당할 CPU와 메모리
execution_role_arn ECS가 이미지 pull, 로그 전송에 사용할 Role
task_role_arn 컨테이너 애플리케이션이 사용할 Role
container_definitions 컨테이너 이미지, 포트, 환경 변수, 로그 설정

Fargate에서는 network_mode = "awsvpc"를 사용한다.

이 모드에서는 Task마다 ENI가 생성되고, Security Group과 Subnet이 Task에 직접 적용된다.


7. ECS Service 만들기

Task Definition은 컨테이너 실행 방법을 정의한다.

하지만 Task Definition만으로 컨테이너가 계속 실행되는 것은 아니다.

ECS Service가 Task를 원하는 개수만큼 유지한다.

resource "aws_ecs_service" "app" {
  name            = "${var.project_name}-${var.environment}-app-service"
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.app.arn

  desired_count = 2
  launch_type   = "FARGATE"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.ecs_task.id]
    assign_public_ip = false
  }

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

여기서 중요한 설정은 desired_count다.

desired_count = 2

이 값은 ECS가 유지해야 하는 Task 개수다.

desired_count = 2
→ Task 2개 유지

Task 1개 장애 발생
→ ECS가 새 Task 실행

Fargate Task를 Private Subnet에 배치하고 assign_public_ip = false로 두면 외부에서 Task에 직접 접근하지 않는다.

Internet
→ ALB
→ ECS Task

8. ALB Target Group과 연결하기

웹 서비스로 ECS를 운영하려면 ALB와 연결하는 경우가 많다.

ECS Service의 load_balancer 블록에서 Target Group을 연결한다.

resource "aws_ecs_service" "app" {
  name            = "${var.project_name}-${var.environment}-app-service"
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.app.arn

  desired_count = 2
  launch_type   = "FARGATE"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.ecs_task.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = var.target_group_arn
    container_name   = "app"
    container_port   = 8080
  }

  depends_on = [
    var.listener_dependency
  ]

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

위 코드에서 load_balancer는 다음 의미다.

Target Group
→ ECS Service의 Task로 트래픽 전달

container_name
→ Task Definition의 컨테이너 이름

container_port
→ 컨테이너가 실제로 listen하는 포트

중요한 점은 Fargate에서는 Target Group의 target_type을 보통 ip로 둔다는 것이다.

EC2 Target Group
→ target_type = "instance"

ECS Fargate Target Group
→ target_type = "ip"

ECS Fargate Task는 EC2 Instance ID가 아니라 Task ENI의 IP로 Target Group에 등록되기 때문이다.

참고로 위 예시의 depends_on = [var.listener_dependency]는 실제 Terraform 코드에서는 변수로 직접 쓰기 어렵다.

같은 코드 안에서 ALB Listener를 함께 만든다면 다음처럼 명시할 수 있다.

depends_on = [
  aws_lb_listener.http
]

만약 ALB를 별도 모듈이나 별도 state에서 관리한다면, 이미 Listener와 Target Group이 생성된 상태에서 ECS Service를 적용하는 방식으로 나누는 것이 좋다.


9. Security Group 구성

ECS Fargate에서는 Task마다 네트워크 인터페이스가 생긴다.

따라서 Task에 Security Group을 직접 연결한다.

보통 구조는 다음과 같다.

ALB Security Group
→ ECS Task Security Group
→ 8080 허용

Terraform 코드는 다음과 같다.

resource "aws_security_group" "ecs_task" {
  name        = "${var.project_name}-${var.environment}-ecs-task-sg"
  description = "Security group for ECS Fargate task"
  vpc_id      = var.vpc_id

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

resource "aws_vpc_security_group_ingress_rule" "ecs_from_alb" {
  security_group_id = aws_security_group.ecs_task.id

  referenced_security_group_id = var.alb_security_group_id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080

  description = "Allow app traffic from ALB"
}

resource "aws_vpc_security_group_egress_rule" "ecs_all" {
  security_group_id = aws_security_group.ecs_task.id

  cidr_ipv4   = "0.0.0.0/0"
  ip_protocol = "-1"

  description = "Allow all outbound traffic"
}

이렇게 하면 ECS Task는 외부 전체가 아니라 ALB에서 오는 요청만 받을 수 있다.

권장 구조:
Internet
→ ALB
→ ECS Task

피하고 싶은 구조:
Internet
→ ECS Task 직접 접근

10. 환경 변수와 Secret 주입하기

ECS Task Definition에서는 일반 환경 변수와 Secret 값을 구분해서 넣을 수 있다.

environment
→ 일반 설정값

secrets
→ Secrets Manager / SSM Parameter Store 값

예를 들어 일반 환경 변수는 다음처럼 작성한다.

environment = [
  {
    name  = "APP_ENV"
    value = var.environment
  },
  {
    name  = "LOG_LEVEL"
    value = "info"
  }
]

Secret 값은 secrets 블록에 넣는다.

secrets = [
  {
    name      = "DB_CREDENTIAL"
    valueFrom = var.db_secret_arn
  },
  {
    name      = "API_TOKEN"
    valueFrom = var.api_token_parameter_arn
  }
]

이 설정을 Task Definition 안에 포함하면 다음과 같다.

container_definitions = jsonencode([
  {
    name  = "app"
    image = "${var.ecr_repository_url}:${var.image_tag}"

    essential = true

    portMappings = [
      {
        containerPort = 8080
        protocol      = "tcp"
      }
    ]

    environment = [
      {
        name  = "APP_ENV"
        value = var.environment
      }
    ]

    secrets = [
      {
        name      = "DB_CREDENTIAL"
        valueFrom = var.db_secret_arn
      }
    ]

    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group         = aws_cloudwatch_log_group.app.name
        awslogs-region        = var.aws_region
        awslogs-stream-prefix = "ecs"
      }
    }
  }
])

Secret을 주입하려면 Task Execution Role에 해당 Secret이나 Parameter를 읽을 권한이 필요하다.

Secrets Manager
→ secretsmanager:GetSecretValue

SSM Parameter Store
→ ssm:GetParameter

KMS 사용 시
→ kms:Decrypt

11. desired_count와 배포 관리

desired_count는 ECS Service가 유지할 Task 개수다.

desired_count = 2

이 값이 2라면 ECS는 정상 Task 2개를 유지하려고 한다.

하나의 Task가 비정상 상태가 되면 새 Task를 실행해서 개수를 맞춘다.

Task 1개 장애
→ ECS가 새 Task 실행
→ desired_count 유지

다만 운영에서는 desired_count를 Terraform이 계속 관리할지 고민해야 한다.

예를 들어 Auto Scaling이나 배포 파이프라인이 ECS Service의 desired count를 변경할 수 있다.

그런데 Terraform 코드에 desired_count = 2가 고정되어 있으면, 다음 apply 때 다시 2로 되돌리려 할 수 있다.

Auto Scaling
→ desired_count = 4

Terraform apply
→ desired_count = 2로 되돌리려 함

이런 경우에는 lifecycle의 ignore_changes를 고려할 수 있다.

resource "aws_ecs_service" "app" {
  name            = "${var.project_name}-${var.environment}-app-service"
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.app.arn

  desired_count = 2

  lifecycle {
    ignore_changes = [
      desired_count
    ]
  }
}

다만 초보 단계에서는 먼저 Terraform이 desired_count를 관리하는 방식으로 이해하는 것이 좋다.

Auto Scaling이나 배포 자동화를 도입한 뒤에 ignore_changes를 적용할지 판단하면 된다.


12. 실전 예제: ALB → ECS Fargate 구조

이제 지금까지의 내용을 하나로 합쳐보자.

구조는 다음과 같다.

Internet
→ ALB
→ Target Group
→ ECS Service
→ ECS Fargate Task
→ ECR Image

12.1 variables.tf

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

variable "environment" {
  description = "Environment name"
  type        = string
}

variable "aws_region" {
  description = "AWS region"
  type        = string
}

variable "vpc_id" {
  description = "VPC ID"
  type        = string
}

variable "private_subnet_ids" {
  description = "Private subnet IDs for ECS tasks"
  type        = list(string)
}

variable "alb_security_group_id" {
  description = "ALB security group ID"
  type        = string
}

variable "target_group_arn" {
  description = "ALB target group ARN"
  type        = string
}

variable "ecr_repository_url" {
  description = "ECR repository URL"
  type        = string
}

variable "image_tag" {
  description = "Docker image tag"
  type        = string
  default     = "latest"
}

variable "desired_count" {
  description = "Desired ECS task count"
  type        = number
  default     = 2
}

12.2 locals.tf

locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
  }

  app_name = "${var.project_name}-${var.environment}-app"
}

12.3 main.tf

resource "aws_ecs_cluster" "app" {
  name = "${var.project_name}-${var.environment}-cluster"

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

resource "aws_cloudwatch_log_group" "app" {
  name              = "/ecs/${var.project_name}/${var.environment}/app"
  retention_in_days = 14

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-${var.environment}-app-log-group"
  })
}

resource "aws_iam_role" "ecs_task_execution" {
  name = "${var.project_name}-${var.environment}-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}-${var.environment}-ecs-task-execution-role"
  })
}

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

resource "aws_iam_role" "ecs_task" {
  name = "${var.project_name}-${var.environment}-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}-${var.environment}-ecs-task-role"
  })
}

resource "aws_security_group" "ecs_task" {
  name        = "${var.project_name}-${var.environment}-ecs-task-sg"
  description = "Security group for ECS Fargate task"
  vpc_id      = var.vpc_id

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

resource "aws_vpc_security_group_ingress_rule" "ecs_from_alb" {
  security_group_id = aws_security_group.ecs_task.id

  referenced_security_group_id = var.alb_security_group_id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080

  description = "Allow traffic from ALB"
}

resource "aws_vpc_security_group_egress_rule" "ecs_all" {
  security_group_id = aws_security_group.ecs_task.id

  cidr_ipv4   = "0.0.0.0/0"
  ip_protocol = "-1"

  description = "Allow all outbound traffic"
}

resource "aws_ecs_task_definition" "app" {
  family                   = local.app_name
  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 = "${var.ecr_repository_url}:${var.image_tag}"

      essential = true

      portMappings = [
        {
          containerPort = 8080
          protocol      = "tcp"
        }
      ]

      environment = [
        {
          name  = "APP_ENV"
          value = var.environment
        }
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.app.name
          awslogs-region        = var.aws_region
          awslogs-stream-prefix = "ecs"
        }
      }
    }
  ])

  tags = merge(local.common_tags, {
    Name = "${local.app_name}-task-definition"
  })
}

resource "aws_ecs_service" "app" {
  name            = "${local.app_name}-service"
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.app.arn

  desired_count = var.desired_count
  launch_type   = "FARGATE"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.ecs_task.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = var.target_group_arn
    container_name   = "app"
    container_port   = 8080
  }

  tags = merge(local.common_tags, {
    Name = "${local.app_name}-service"
  })
}

12.4 outputs.tf

output "ecs_cluster_name" {
  description = "ECS cluster name"
  value       = aws_ecs_cluster.app.name
}

output "ecs_service_name" {
  description = "ECS service name"
  value       = aws_ecs_service.app.name
}

output "ecs_task_definition_arn" {
  description = "ECS task definition ARN"
  value       = aws_ecs_task_definition.app.arn
}

output "ecs_task_security_group_id" {
  description = "ECS task security group ID"
  value       = aws_security_group.ecs_task.id
}

이 구성의 특징은 다음과 같다.

ECS Cluster 생성
Task Execution Role 생성
Task Role 생성
CloudWatch Log Group 생성
ECS Task Security Group 생성
Task Definition 생성
ECS Service 생성
ALB Target Group과 연결
ECR Image 실행

13. 의존성 흐름

ECS Fargate를 Terraform으로 구현할 때의 의존성 흐름은 다음과 같다.

ECR / IAM / Log Group / Security Group
→ Task Definition
→ ECS Service
→ ALB Target Group

이 구조에서 중요한 점은 다음과 같다.

Task Definition은 ECR Image, IAM Role, Log Group을 참조한다.
ECS Service는 Cluster, Task Definition, Security Group, Target Group을 참조한다.
ALB Target Group은 ECS Service를 통해 실행된 Task로 트래픽을 전달한다.
ECS Service는 Task Definition을 실행하고, ALB Target Group과 연결되어 외부 요청을 Task로 전달한다.

14. 자주 하는 실수

14.1 Fargate에서 network_mode를 awsvpc로 설정하지 않음

Fargate에서는 network_mode = "awsvpc"를 사용해야 한다.

network_mode = "awsvpc"

이 설정을 통해 Task마다 ENI가 생성되고, Subnet과 Security Group이 Task에 직접 적용된다.


14.2 ECS Fargate Target Group을 instance 타입으로 만듦

ECS Fargate는 보통 Target Group의 target_typeip로 사용한다.

EC2 대상
→ target_type = "instance"

ECS Fargate 대상
→ target_type = "ip"

Fargate Task는 EC2 Instance ID가 아니라 Task ENI의 IP로 Target Group에 등록되기 때문이다.


14.3 Execution Role과 Task Role을 헷갈림

Execution Role과 Task Role은 용도가 다르다.

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

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

애플리케이션이 S3나 Secrets Manager를 직접 호출한다면 Task Role에 권한을 줘야 한다.


14.4 ECS Task Security Group에서 ALB 접근을 허용하지 않음

ALB가 ECS Task에 접근하려면 ECS Task Security Group에서 ALB Security Group을 허용해야 한다.

ALB Security Group
→ ECS Task Security Group 8080 허용

이 설정이 없으면 ALB는 정상적으로 요청을 받아도 ECS Task로 전달하지 못한다.


14.5 Health Check 경로가 애플리케이션과 맞지 않음

Target Group의 Health Check 경로와 애플리케이션의 실제 health endpoint가 맞아야 한다.

Target Group health_check path = "/health"

Application endpoint 없음
→ Target unhealthy

Spring Boot Actuator를 사용한다면 다음처럼 맞출 수 있다.

health_check path = "/actuator/health"

14.6 Private Subnet에 Task를 두고 NAT Gateway나 VPC Endpoint를 고려하지 않음

Private Subnet에 ECS Task를 두고 assign_public_ip = false로 설정하면 Task는 인터넷에 직접 나갈 수 없다.

하지만 ECR 이미지 pull, CloudWatch Logs 전송, 외부 API 호출 등이 필요할 수 있다.

ECS Task
→ ECR pull
→ CloudWatch Logs 전송
→ 외부 API 호출

이 경우 NAT Gateway나 VPC Endpoint를 고려해야 한다.

Private ECS Task
→ NAT Gateway
→ Internet

또는

Private ECS Task
→ VPC Endpoint
→ ECR / CloudWatch Logs

14.7 latest 태그만 사용함

ECS Task Definition에서 latest 태그만 사용하면 어떤 이미지가 배포되었는지 추적하기 어렵다.

비추천:
app:latest

추천:
app:git-sha-abc1234
app:v1.0.0

운영에서는 Git SHA나 버전 번호처럼 고유한 image tag를 사용하는 것이 좋다.


14.8 Terraform apply만으로 새 이미지가 자동 배포된다고 생각함

ECR에 새 이미지를 push했다고 해서 ECS Service가 자동으로 새 이미지를 실행하는 것은 아니다.

Task Definition의 image tag가 바뀌거나, ECS Service 배포가 트리거되어야 새 Task가 실행된다.

docker push
→ ECR 이미지 저장

ECS 새 배포
→ 새 이미지로 Task 실행

이 흐름은 이후 CI/CD 시리즈에서 자세히 다루는 것이 좋다.


14.9 desired_count를 Terraform과 Auto Scaling이 동시에 관리함

Auto Scaling이 desired_count를 변경하는 구조라면 Terraform이 그 값을 다시 되돌리려 할 수 있다.

이 경우 필요에 따라 ignore_changes를 고려한다.

lifecycle {
  ignore_changes = [
    desired_count
  ]
}

다만 처음에는 Terraform이 desired_count를 관리하도록 두고, Auto Scaling을 도입할 때 별도로 조정하는 것이 이해하기 쉽다.


15. 마무리

이번 글에서는 Terraform으로 ECS Fargate를 구현하는 방법을 정리했다.

ECS Fargate는 여러 리소스가 함께 연결되어 동작한다.

ECR
→ 이미지 저장

Task Definition
→ 컨테이너 실행 설정

ECS Service
→ Task 개수 유지

ALB Target Group
→ 외부 트래픽 전달

Security Group
→ ALB와 Task 간 접근 제어

IAM Role
→ 이미지 pull, 로그 전송, AWS API 접근 권한

처음에는 리소스가 많아 보이지만, 역할을 나누어 보면 구조는 명확하다.

이미지는 ECR에 저장한다.
실행 방법은 Task Definition에 정의한다.
실행 상태는 ECS Service가 유지한다.
외부 요청은 ALB Target Group을 통해 들어온다.

한 줄 정리

ECS Fargate는 ECR 이미지를 Task Definition으로 실행하고, ECS Service가 원하는 개수만큼 유지하며, ALB를 통해 외부 요청을 전달받는 구조다.


다음 글에서는 Lambda와 EventBridge를 Terraform으로 구현해본다. Lambda는 서버를 직접 관리하지 않고 코드를 실행하는 서비스이고, EventBridge를 사용하면 정해진 시간이나 이벤트에 따라 Lambda를 실행할 수 있다.