본문으로 바로가기

배포 흐름

Terraform 코드

코드의 가독성과 독자의 이해력을 높이기 위해 필요하지 않은 기타 인프라 리소스는 제외되어 있는 코드임.

  • ecs.tf
### ECS Service
resource "aws_ecs_service" "api" {
  name            = "${local.name_prefix}-api"
  cluster         = aws_ecs_cluster.designhub.name
  task_definition = aws_ecs_task_definition.api.id
  enable_execute_command = true
  launch_type     = "FARGATE"
  desired_count   = 1

  network_configuration {
    subnets         = local.private_subnets
    security_groups = [local.sg-allow-int-all]
  }

  load_balancer {
    target_group_arn = module.alb_api.target_group_arns[0]
    container_name = aws_ecs_task_definition.api.family
    container_port = 443
  }

  deployment_controller {
    type = "CODE_DEPLOY"
  }

  lifecycle {
    ignore_changes = [
      task_definition,
      desired_count,
      load_balancer
    ]
  }
}
  • alb.tf
### ALB
module "alb_api" {
  source = "../../../modules/alb"

  name = "${local.name_prefix}-api"

  load_balancer_type = "application"
  vpc_id             = local.vpc_id
  subnets            = local.public_subnets
  security_groups = [
    local.sg-allow-ext-http,
    local.sg-allow-idc-all,
    local.sg-allow-office-all
  ]

  http_tcp_listeners = [
    {
      port        = 80
      protocol    = "HTTP"
      action_type = "redirect"
      redirect = {
        port        = "443"
        protocol    = "HTTPS"
        status_code = "HTTP_301"
      }
    }
  ]
  ### Disabled Using Code Deploy
  # https_listeners = [
  #   {
  #     port               = 443
  #     protocol           = "HTTPS"
  #     certificate_arn    = local.certificate_arn
  #   }
  # ]

  target_groups = [
    {
      name                 = "${local.name_prefix}-api-a"
      backend_protocol     = "HTTPS"
      backend_port         = 443
      target_type          = "ip"
      deregistration_delay = 30
      health_check = {
        enabled             = true
        interval            = 30
        path                = "/"
        port                = "80"
        healthy_threshold   = 3
        unhealthy_threshold = 3
        timeout             = 6
        protocol            = "HTTP"
        matcher             = "200"
      }
    },
    {
      name                 = "${local.name_prefix}-api-b"
      backend_protocol     = "HTTPS"
      backend_port         = 443
      target_type          = "ip"
      deregistration_delay = 30
      health_check = {
        enabled             = true
        interval            = 30
        path                = "/"
        port                = "80"
        healthy_threshold   = 3
        unhealthy_threshold = 3
        timeout             = 6
        protocol            = "HTTP"
        matcher             = "200"
      }
    }
  ]

  tags = local.tags
}

resource "aws_lb_listener" "api" {
  load_balancer_arn = module.alb_api.lb_arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = local.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = module.alb_api.target_group_arns[0]
  }

  lifecycle {
    ignore_changes = [
      default_action[0].target_group_arn
    ]
  }
}
  • codedeploy.tf
### Codedeploy
resource "aws_codedeploy_app" "api" {
  compute_platform = "ECS"
  name = var.project
}
resource "aws_codedeploy_deployment_group" "main" {
  app_name              = var.project
  deployment_group_name = var.environment
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
  service_role_arn      = local.iam_role_codedeploy

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout    = "CONTINUE_DEPLOYMENT"
    }

    terminate_blue_instances_on_deployment_success {
      action = "TERMINATE"
      termination_wait_time_in_minutes = 1
    }
  }

  ecs_service {
    cluster_name = aws_ecs_cluster.designhub.name
    service_name = aws_ecs_service.api.name
  }

  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }


  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [
          aws_lb_listener.api.arn
        ]
      }

      target_group {
        name = module.alb_api.target_group_names[0]
      }

      target_group {
        name = module.alb_api.target_group_names[1]
      }
    }
  }
}

 

배포 파이프라인

이 글에서는 빌드 파이프라인 부분은 생략했고 배포 파이프라인 부분만 다룬다.

 

Bamboo 에이전트에서 aws cli로 codedeploy를 트리거링 하여 배포를 하는 방식으로 배포한다.

(Jenkins도 UI나 용어만 다를 뿐 메커니즘은 동일하게 가져가면 된다.)

 

Codedeploy의 배포는 어떤 Task Definition을 사용하여 어떤 로드밸런서를 사용할건지 등이 정의되어 있는 YAML 형식의 파일을 만들고 Input 해줘야 한다.

 

  1. codedeploy에 필요한 yaml 파일을 빌드 에이전트의 로컬에 생성한다.
  2. aws deploy create-deployment 명령어의 --cli-input-yaml 옵션을 이용하여 로컬에 생성된 yaml 파일을 지정하고 트리거링 한다.
# define codedeploy.yml
cat <<EOF > codedeploy.yml
applicationName: '${bamboo.PROJECT}'
deploymentGroupName: '${bamboo.ENVIRONMENT}'
deploymentConfigName: CodeDeployDefault.ECSAllAtOnce
revision:
  revisionType: AppSpecContent
  appSpecContent:
    content: |
      version: 0.0
      Resources:
        - TargetService:
            Type: AWS::ECS::Service
            Properties:
              TaskDefinition: "arn:aws:ecs:ap-northeast-2:${bamboo.AWS_ACCOUNT_ID_SECRET}:task-definition/${bamboo.PROJECT}-${bamboo.ENVIRONMENT}-api"
              LoadBalancerInfo:
                ContainerName: "${bamboo.PROJECT}-${bamboo.ENVIRONMENT}-api"
                ContainerPort: 443
EOF

aws deploy create-deployment \
  --cli-input-yaml file://codedeploy.yml

여기서 중요한 포인트

위 구성에서는 CodeDeploy의 Deploy를 aws cli로 실행하는데 배포가 완료되면 ALB 리스너에서는 전에 사용하던 Target Group의 연결을 끊고 새로 배포된 컨테이너가 존재하는 Target Group으로 연결이 된다.

그런데 라폼이에는 위의 상태가 저장이 안된다. 그러므로 위의 과정 이후에 Terraform Apply를 치면 전에 사용하던 Target Group을 다시 스위칭하는 현상이 발생한다. (Replcae 발생)

따라서 특정 Attribute에 변경사항이 일어나도 무시하는 lifecycle을 추가해줘야 하고 아래와 같이 aws_lb_listener 리소스에 ignore_chages를 설정해줘야 한다.

resource "aws_lb_listener" "api" {
...
  lifecycle {
    ignore_changes = [
      default_action[0].target_group_arn
    ]
  }
}

aws_lb_listener 리소스 뿐만 아니라 aws_ecs_service 에도 아래와 같이 ignore_changes 를 설정해줘야한다.

resource "aws_ecs_service" "api" {
...
  lifecycle {
    ignore_changes = [
      task_definition,
      desired_count,
      load_balancer
    ]
  }
}

이렇게 설정하면 Terraform가 관리하는 스콮 이외의 상태변화에도 Replace가 발생하지 않는다.

 

Reference