티스토리 뷰

 

 

개요

AWS ECS 기초부터 파악해보기에서도 이야기했지만, 이번 프로젝트는 ECS로 구축했다. 그런데, 작업을 시작할 당시에 아직 상용 서버용 AWS  계정을 전달 받지 못 했고, 개발 서버를 운영해야한다는 문제가 있었다. 이럴 경우 계정을 옮겨가야 하는데 인프라 구성을 AWS 콘솔로 작업할 경우, 개발 서버의 인프라를 상용 서버와 동일하게 구성하기 어려울 수 있다.

 

워낙 작업이 많아서 뭔가 놓칠 가능성이 크고, 특히 IAM과 보안 그룹이 싱크가 안맞아서 예상치 못한 문제가 생길 여지가 있다. 그래서 개발 서버에서의 모든 인프라를 Terraform으로 구성해 상용 서버용 계정을 전달 받았을 때 바로 옮길 수 있게 작업 했고, 이 글은 이 내용을 정리했다.

 

들어가기전에

1. Terraform 리소스는 S3에 Terraform Backend에 저장해서 사용했다. 

2. VPC는 이미 구성되어 있다고 가정하고 시작한다. 옛날에 작업했던 것과 거의 동일하게 구성했다.

2024.04.18 - [개발/인프라] - Terraform으로 EKS 배포하기 1. AWS VPC 셋업

2024.04.25 - [개발/인프라] - Terraform으로 EKS 배포하기 2. AWS VPC 셋업 추가 작업

3. IAM과 보안 그룹도 그렇게 자세하게 다루면 너무 늘어질 것 같아서 가볍게 전달해보려고 한다.

4. 프로바이더는 당연히 필요하니까 생략한다.

5. 범용적으로 쓴 local 파일은 다음과 같다.

locals {
  vpc =            data.terraform_remote_state.vpc.outputs.vpc
  security_group = data.terraform_remote_state.security_group.outputs
  iam =            data.terraform_remote_state.iam.outputs
}

 

인프라 구성

출처 : https://sakyasumedh.medium.com/setup-application-load-balancer-and-point-to-ecs-deploy-to-aws-ecs-fargate-with-load-balancer-4b5f6785e8f

인프라 구성은 가장 보편적인 위 그림과 같이 보편적으로 알려진 방식으로 구축했다. 

1. ALB

resource "aws_lb" "my_app_alb" {
  name               = "my-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [local.security_group.alb_sg_id]
  subnets            = local.vpc.public_subnets
}

resource "aws_lb_target_group" "my_app_tg" {
  name        = "my-target-group"
  port        = 8080
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = local.vpc.vpc_id

  health_check {
    path                = "/actuator/health"
    interval            = 30
    timeout             = 5
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }
}

resource "aws_lb_listener" "my_app_listener" {
  load_balancer_arn = aws_lb.my_app_alb.arn
  port              = 80
  protocol          = "HTTP"

  depends_on = [
    aws_lb_target_group.my_app_tg
  ]
}

resource "aws_lb_listener_rule" "my_app_listener_rule" {
  listener_arn = aws_lb_listener.my_app_listener.arn
  priority     = 1

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.my_app_tg.arn
  }

  condition {
    host_header {
      values = ["dev.myapp.com"]
    }
  }

  depends_on = [
    aws_lb_listener.my_app_listener,
    aws_lb_target_group.my_app_tg
  ]
}

ALB와 타겟그룹, 리스너, 리스너 룰을 함께 지정했다. 헬스체크 URL을 잘 만드는게 중요하다.

 

그런데 뭔가 실수했을 때 다시만들려고 terraform destroy를 할 때 타겟 그룹과 리스너가 삭제가 잘 안되서 수동을 매번 지워줘야하니 한번에 잘 할 수 있도록 하자.

 

2. ECS Cluster

module "ecs_cluster" {
  source  = "terraform-aws-modules/ecs/aws"
  version = "~> 4.0"

  cluster_name = "my-app-cluster"
}

ECS는 Cluster라는 가장 큰 가상 영역으로 구분된다. 

 

3. ECS Service

service는 task에 정의될 컨테이너와 외부가 어떻게 연결되는지를 정의하면 된다.

resource "aws_ecs_service" "my_app_backend_service" {
  name            = "my-app-backend-service"
  cluster         = module.ecs_cluster.cluster_id
  task_definition = aws_ecs_task_definition.my_app_backend_task.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = local.vpc.private_subnets
    security_groups = [local.security_group.backend_sg_id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.my_app_tg.arn
    container_name   = "my-app-backend"
    container_port   = 8080
  }

  deployment_minimum_healthy_percent = 50
  deployment_maximum_percent         = 200

  lifecycle {
    ignore_changes = [task_definition]
  }
}

이번에는 Fargate를 써보려고 launch_type을 Fargate로 명시했다. EC2로 구성하게 된다면 현재와 다르게 구성해야한다.

 

task_definition을 무시한 이유는 인프라 작업이 되면 디폴트로 작업해둔 Task Definition에 지정해 놓은 이미지로 바뀌면서, 매번 재배포되는 문제가 있었다. 그래서 변경을 무시하도록 작업해뒀다.

 

4. ECS Task Definition

콘솔에서 작업하면 상당히 복잡하지만 테라폼에서 제공하는 리소스를 쓰면 간단하게 만들 수 있다. 

resource "aws_ecs_task_definition" "my_app_backend_task" {
  family                   = "my-app-backend-task"
  network_mode             = "awsvpc" # Fargate는 awsvpc를 사용
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"  
  memory                   = "1024"
  execution_role_arn       = local.iam.ecs_task_execution_role_arn
  task_role_arn            = local.iam.ecs_task_role_arn

  container_definitions = jsonencode([
    {
      name      = "my-app-backend"
      image     = "[ECR Repo]/myapp-backend:latest"
      cpu       = 512
      memory    = 1024
      essential = true
      portMappings = [
        {
          containerPort = 8080
          hostPort      = 8080
          protocol      = "tcp"
        }
      ]
      environment = [
        {
          name  = "SPRING_PROFILES_ACTIVE"
          value = "dev"
        }
      ]
    }
  ])
}

ECR 레포지토리만 지정해주면 거기있는 이미지를 가져온다. CI/CD를 구축할 때 Task Definition에 ECR에 이미지 경로를 갱신하고 재배포하게 유도하니 잘 알아둬야 한다.

6. IAM

resource "aws_iam_role" "ecs_task_role" {
  name = "notes-ecs-task-role"

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

resource "aws_iam_policy" "ecs_task_policy" {
  name = "ecs-task-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = "s3:*" # 모든 S3 작업 허용
        Effect   = "Allow"
        Resource = "*"    # 모든 S3 리소스
      },
      {
        Action   = "rds:*" # 모든 RDS 작업 허용
        Effect   = "Allow"
        Resource = "*"     # 모든 RDS 리소스
      },
      {
        Action   = [
          "ssm:GetParameter",
          "ssm:GetParameters",
          "ssm:GetParametersByPath"
        ],
        Effect   = "Allow",
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_policy_attachment" "ecs_task_policy_attachment" {
  name       = "ecs-task-policy-attachment"
  roles      = [aws_iam_role.ecs_task_role.name]
  policy_arn = aws_iam_policy.ecs_task_policy.arn
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name = "ecs-task-execution-role"

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

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

task에겐 S3, RDS, SSM(파라미터 스토어용) 권한을 줬다. Task Definition에 IAM에 정의한 ecs_task_role_arn, ecs_task_execution_role_arn를 사용하면 된다. 생각보다 IAM 구성이 간단하다.

 

마치며

EKS를 처음 구성할 당시에 비해 이미 해본 경험이 있어선지, ECS를 이용한 서버구축 자체는 그렇게 어렵지 않았다.

 

그런데, ArgoCD를 ECS와 연동하려면 온몸을 비틀어야해서 제외하기로 해서 결국 CI/CD 구성이 바뀌게 되었다.

 

GitHub Actions에서 좋은 workflow를 제공해서 결과적으로는 완성할 수 있었지만,

 

CI/CD를 위한 작업들에서 조금 생각할 부분들이 있었어서 다음 글에 이 내용을 다뤄보겠다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함