테라폼 실전 강좌 #8 ECS Fargate 배포: 태스크 정의, 서비스, ALB 뒤에서의 전환

4 분 소요

지금의 컴퓨팅 계층은 잘 동작하지만 배포 이야기를 시작하면 한계가 보입니다. 앱을 갱신하려면 user_data를 고쳐 인스턴스를 전부 교체해야 하고, 부팅 때마다 패키지 설치가 돌므로 뜨는 시간도 깁니다. 앱을 이미지로 빌드해 그대로 배포하는 컨테이너 방식이 이 문제의 표준 답이고, AWS에서 서버 관리 없이 컨테이너를 돌리는 선택지가 ECS Fargate입니다(EC2 모드와의 비교는 ECS Fargate vs EC2에 정리해 두었습니다). 이번 편의 테라폼 관점 하이라이트는 운영 중인 서비스의 컴퓨팅 계층을 옆에 세워 두고 갈아타는 전환 시나리오입니다.

클러스터와 태스크 정의 #

앱 이미지는 준비되어 있다고 전제합니다(실습은 공개 nginx 이미지로 대신합니다. ECR 구축과 이미지 푸시는 CI의 영역이라 운영 시리즈에서 다룹니다).

ecs.tf
# ecs.tf (새 파일)
resource "aws_ecs_cluster" "main" {
  name = "myapp-${var.env}"
}

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

  execution_role_arn = aws_iam_role.ecs_execution.arn
  task_role_arn      = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = "web"
      image = "public.ecr.aws/nginx/nginx:latest"
      portMappings = [{ containerPort = 80 }]
    }
  ])
}

container_definitions는 provider가 HCL 블록으로 풀지 않은 부분이라 기초 #4의 jsonencode가 다시 나옵니다. 역할이 두 개인 것이 ECS의 유명한 혼동 지점이라 구분해 둡니다.

  • execution role: ECS 인프라가 태스크를 띄우려고 쓰는 권한. 이미지 풀, 로그 전송. 대부분 AWS 관리형 정책 하나로 충분합니다.
  • task role: 컨테이너 안의 앱이 쓰는 권한. SSM 파라미터 읽기, S3 접근. 지난 편에서 EC2 역할에 줬던 권한이 여기로 옮겨 옵니다.

EC2 시절 인스턴스 프로파일 하나에 뭉쳐 있던 권한이 “띄우는 쪽"과 “앱 쪽"으로 나뉜 것이라고 이해하면 정확합니다. 역할 코드는 지난 편의 패턴 그대로라 생략하되, ecs_task 역할의 권한 정책은 web 역할의 것을 재사용합니다.

서비스와 ip 타입 대상 그룹 #

ECS 서비스는 ASG의 컨테이너판입니다. 원하는 태스크 수를 유지하고, 죽으면 다시 띄우고, ALB에 등록합니다. 여기서 대상 그룹을 새로 만들어야 하는 이유가 있습니다. #4의 대상 그룹은 인스턴스를 등록하는 기본 타입인데, Fargate 태스크는 인스턴스가 아니라 ENI(IP)로 등록되므로 target_type = "ip"인 대상 그룹이 필요합니다.

ecs.tf
resource "aws_lb_target_group" "ecs" {
  name_prefix = "ecs-"
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = aws_vpc.main.id

  health_check {
    path = "/"
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_ecs_service" "web" {
  name            = "web"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.web.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = [for s in aws_subnet.private : s.id]
    security_groups = [aws_security_group.web.id]
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.ecs.arn
    container_name   = "web"
    container_port   = 80
  }
}

네트워크 구성이 낯익을 것입니다. 같은 프라이빗 서브넷, 같은 web 보안 그룹입니다. awsvpc 모드의 태스크는 자기 ENI를 갖는 어엿한 네트워크 구성원이라, #3에서 세운 보안 그룹 체이닝이 그대로 적용됩니다. 컨테이너로 바꿔도 네트워크 설계가 재사용되는 것은 그 설계가 계층 기준이었기 때문입니다.

전환: 같은 ALB 뒤에서 갈아타기 #

지금 상태를 보면 ALB 뒤에 대상 그룹이 둘입니다. 리스너는 아직 EC2 쪽(aws_lb_target_group.web)을 보고 있고, ECS 태스크들은 자기 대상 그룹에서 헬스 체크만 받으며 대기 중입니다. 전환은 리스너의 한 줄입니다.

lb.tf
# lb.tf 수정
resource "aws_lb_listener" "http" {
  # ...

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.ecs.arn   # web → ecs
  }
}

plan에는 리스너 수정 하나만 잡히고, apply 순간부터 트래픽이 컨테이너로 흐릅니다. 문제가 생기면 참조를 되돌려 apply 하면 즉시 롤백입니다. 갈아탄 것이 확인되면 EC2 계층(ASG, launch template, 옛 대상 그룹, 스케일링 정책)을 코드에서 지웁니다. #4에서 인스턴스 하나를 지울 때 본 흐름의 확장판인데, 이번에는 지워지는 리소스가 여럿이니 plan의 destroy 목록을 한 줄씩 확인하고 apply 합니다. 운영이었다면 이 전환 사이에 관찰 기간을 두겠지만, 절차 자체는 동일합니다.

정리 #

이번 글에서 다룬 내용입니다.

  • user_data 방식은 배포마다 인스턴스 교체와 긴 부팅을 요구합니다. 이미지로 빌드해 배포하는 컨테이너가 표준 답이고, 서버 관리가 없는 쪽이 Fargate입니다
  • 태스크 정의의 execution role은 ECS 인프라의 권한, task role은 앱의 권한입니다. 앱 권한은 EC2 역할에서 task role로 옮겨 옵니다
  • Fargate 태스크는 IP로 등록되므로 target_type이 ip인 대상 그룹을 새로 만듭니다
  • awsvpc 모드 태스크는 기존 프라이빗 서브넷과 보안 그룹 체이닝을 그대로 재사용합니다
  • 전환은 리스너의 대상 그룹 참조 한 줄입니다. 확인 후 EC2 계층을 코드에서 지우고, plan의 destroy 목록을 검토하며 마무리합니다

다음 글(#9 환경 분리)에서는 지금까지 dev 하나로 키워 온 코드를 모듈로 리팩터링하고, prod 환경을 나란히 세웁니다. 미뤄 둔 “실습과 운영의 값 차이"가 전부 여기서 해소됩니다.

X