[AWS] ECS: Blue Green ๋ฐฐํฌ ๊ตฌ์„ฑ with Terraform

ECS๋กœ ์„œ๋ฒ„๋ฅผ ๊ตฌ์„ฑํ•˜๊ฒŒ ๋˜๋ฉด, ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” rolling update๋กœ ๋ฐฐํฌ๋ฅผ ํ•˜๊ฒŒ ๋œ๋‹ค.
์—ฌ๊ธฐ์—๋Š” ์กฐ๊ธˆ ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค. ๋ฐฐํฌ๋ฅผ ํ•˜๊ณ  ๊ตฌ๋ฒ„์ „ ๋…ธ๋“œ๊ฐ€ ๋‚ด๋ ค๊ฐ€๊ธฐ ์ „๊นŒ์ง€๋Š” ๊ตฌ๋ฒ„์ „๊ณผ ์‹ ๋ฒ„์ „ ๋…ธ๋“œ๊ฐ€ ๊ณต์กดํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
์ด๊ฑธ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด blue-green ๋ฐฐํฌ ์ „๋žต์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

CodeDeploy๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  target group์„ ๋น„๋กฏํ•œ ๋ช‡๊ฐ€์ง€ ๊ตฌ์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ด๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋Ÿฐ ๊ตฌ์กฐ๋‹ค.

AWS์—์„œ๋Š” target group์„ ์ด์šฉํ•ด์„œ blue์™€ green์„ ๊ตฌํ˜„ํ•œ๋‹ค.
ํ™€์ˆ˜๋ฒˆ์งธ ๋ฐฐํฌํ• ๋•Œ๋Š” blue๋กœ ๋ฐฐํฌํ•˜๊ณ , ์ง์ˆ˜๋ฒˆ์งธ ๋ฐฐํฌํ• ๋•Œ๋Š” green์œผ๋กœ ๋ฐฐํฌํ•˜๋ฉด์„œ, ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ ๋ฆฌ์Šค๋„ˆ๋งŒ ๋•Œ์— ๋งž๋Š” target group์„ ๊ฐ€๋ฆฌํ‚ค๊ฒŒ ์Šค์œ„์นญํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

์—ฌ๊ธฐ์„œ๋Š” terraform์„ ์ด์šฉํ•ด์„œ ๊ตฌ์„ฑํ•ด๋ณด๊ฒ ๋‹ค.
์ „์ฒด ์ฝ”๋“œ๋Š” ๋‚ด ๊ฐœ์ธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์— ์žˆ๋‹ค.
https://github.com/myyrakle/terraform/tree/master/aws/ecs-server/blue-green

๋จผ์ € ํƒ€๊ฒŸ๊ทธ๋ฃน์„ 2๊ฐœ ์„ธํŒ…ํ•ด์•ผ ํ•œ๋‹ค. rolling update์—์„œ๋Š” ํ•˜๋‚˜๋ฉด ์ถฉ๋ถ„ํ–ˆ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ๋Š” 2๊ฐœ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
๋™์ผํ•˜๊ฒŒ ๊ตฌ์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

// ๋กœ๋“œ๋ฐธ๋Ÿฐ์‹ฑ์— ์‚ฌ์šฉํ•  ๋Œ€์ƒ ๊ทธ๋ฃน
resource "aws_lb_target_group" "target_group_blue" {
  name             = "${local.resource_id}-blue"
  port             = var.target_group_port
  protocol_version = var.target_group_protocol_version
  protocol         = var.target_group_protocol
  vpc_id           = var.vpc_id
  target_type      = "ip"

  health_check {
    enabled             = true
    path                = var.healthcheck_uri
    interval            = var.healthcheck_interval
    protocol            = var.target_group_protocol
    healthy_threshold   = 5
    unhealthy_threshold = 2
    timeout             = 20
  }

  tags = local.tags
}

// Blue Green ๋ฐฐํฌ์— ์‚ฌ์šฉํ•  ๋Œ€์ƒ ๊ทธ๋ฃน
resource "aws_lb_target_group" "target_group_green" {
  name             = "${local.resource_id}-green"
  port             = var.target_group_port
  protocol_version = var.target_group_protocol_version
  protocol         = var.target_group_protocol
  vpc_id           = var.vpc_id
  target_type      = "ip"

  health_check {
    enabled             = true
    path                = var.healthcheck_uri
    interval            = var.healthcheck_interval
    protocol            = var.target_group_protocol
    healthy_threshold   = 5
    unhealthy_threshold = 2
    timeout             = 20
  }

  tags = local.tags
}

๊ทธ๋ฆฌ๊ณ  ๋ฆฌ์Šค๋„ˆ๋ฅผ 2๊ฐœ ์„ค์ •ํ•ด์ค€๋‹ค.
ํ•˜๋‚˜๋Š” ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ์šฉ, ํ•˜๋‚˜๋Š” ํ…Œ์ŠคํŠธ์šฉ์ด๋‹ค.

// HTTP ๋ฆฌ์Šค๋„ˆ
resource "aws_lb_listener" "http_test_listener" {
  load_balancer_arn = aws_lb.loadbalancer.arn
  port              = "8080"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.target_group_green.arn
  }

  lifecycle {
    ignore_changes = [default_action]
  }

  tags = local.tags
}

<br>

// HTTPS ๋ฆฌ์Šค๋„ˆ
resource "aws_lb_listener" "https_listener" {
  load_balancer_arn = aws_lb.loadbalancer.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = var.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.target_group_blue.arn
  }

  lifecycle {
    ignore_changes = [default_action]
  }

  tags = local.tags
}

SSL์„ ์ ์šฉํ•˜์ง€ ์•Š์„ ๊ฑฐ๋ผ๋ฉด 443๋งŒ 80์œผ๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค.

๊ทธ๋ฆฌ๊ณ  lifecycle์„ ์ด์šฉํ•ด์„œ default_action์„ ์ฒ˜์Œ ์ƒ์„ฑ ์ดํ›„์—๋Š” ๊ฑด๋“ค์ง€ ์•Š๋„๋ก ํ–ˆ๋‹ค.
์ €๊ธฐ์„œ target_group_arn์ด ๋ฐฐํฌํ• ๋•Œ๋งˆ๋‹ค ๊ณ„์† ๋ฐ”๋€” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋‹ค์Œ์€ ๋ฆฌ์Šค๋„ˆ๋‹ค.

// HTTP ๋ฆฌ์Šค๋„ˆ 
// ํŠธ๋ž˜ํ”ฝ ํ…Œ์ŠคํŠธ
resource "aws_lb_listener" "http_test_listener" {
  load_balancer_arn = aws_lb.loadbalancer.arn
  port              = "8080"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.target_group_green.arn
  }

  lifecycle {
    ignore_changes = [default_action]
  }

  tags = local.tags
}

<br>

// HTTPS ๋ฆฌ์Šค๋„ˆ
resource "aws_lb_listener" "https_listener" {
  load_balancer_arn = aws_lb.loadbalancer.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = var.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.target_group_blue.arn
  }

  lifecycle {
    ignore_changes = [default_action]
  }

  tags = local.tags
}

๋ฆฌ์Šค๋„ˆ์—๋Š” ํŠธ๋ž˜ํ”ฝ ํ…Œ์ŠคํŠธ์šฉ ๋ฆฌ์Šค๋„ˆ ํ•˜๋‚˜์™€ ๋ฉ”์ธ ๋ฆฌ์Šค๋„ˆ ํ•˜๋‚˜๋ฅผ ๋’€๋‹ค.
์ €๊ธฐ์„œ๋Š” ๋ฉ”์ธ ๋ฆฌ์Šค๋„ˆ๋ฅผ https ํ•˜๋‚˜๋งŒ ๋’€๋Š”๋ฐ, ssl์„ ์“ฐ์ง€ ์•Š์„ ๊ฑฐ๋ผ๋ฉด http๋กœ ๋ฐ”๊พธ๋ฉด ๋œ๋‹ค.

https์™€ http๋ฅผ ์ „๋ถ€ ๋ฆฌ์Šค๋‹ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ์šฉ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

// HTTP ๋ฆฌ์Šค๋„ˆ
resource "aws_lb_listener" "http_listener" {
  load_balancer_arn = aws_lb.loadbalancer.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }

  tags = local.tags
}

๊ทธ๋ฆฌ๊ณ  ์„œ๋น„์Šค์—๋„ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
๋ฐฐํฌ๋ฅผ codedeploy์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋ž˜ ๊ตฌ๋ฌธ์„ ์‚ฝ์ž…ํ•ด์ค€๋‹ค.

  deployment_controller {
    type = "CODE_DEPLOY"
  }

๋ฐฐํฌ ์ •์ฑ…์„ ๊ฒฐ์ •ํ•  deploy config๋ฅผ ํ•˜๋‚˜ ์ถ”๊ฐ€ํ•ด์ฃผ๊ณ 

resource "aws_codedeploy_deployment_config" "config_deploy" {
  deployment_config_name = local.resource_id
  compute_platform       = "ECS"

  traffic_routing_config {
    type = "AllAtOnce" // ํ•œ๋ฒˆ์— ๊ต์ฒด
  }
}

deploy group์„ ์ •์˜ํ•œ๋‹ค.
์ด๊ฒŒ blue-green ๋ฐฐํฌ๋ฅผ ์ฃผ๊ด€ํ•  ๋…€์„์ด๋‹ค.

// blue-green ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ code deploy group
resource "aws_codedeploy_deployment_group" "deployment_group" {
  app_name               = aws_codedeploy_app.deploy.name
  deployment_config_name = aws_codedeploy_deployment_config.config_deploy.deployment_config_name
  deployment_group_name  = local.resource_id
  service_role_arn       = aws_iam_role.codedeploy_role.arn

  // ์‹คํŒจ์‹œ ๋กค๋ฐฑ
  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout    = "CONTINUE_DEPLOYMENT"
      wait_time_in_minutes = 0
    }

    // green ๋ฐฐํฌ ์„ฑ๊ณต์‹œ blue ์ธ์Šคํ„ด์Šค๋ฅผ 5๋ถ„ ํ›„์— ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
    terminate_blue_instances_on_deployment_success {
      action                           = "TERMINATE"
      termination_wait_time_in_minutes = 5
    }
  }

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

  ecs_service {
    cluster_name = aws_ecs_cluster.ecs_cluster.name
    service_name = aws_ecs_service.ecs_service.name
  }

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

      test_traffic_route {
        listener_arns = [aws_lb_listener.http_test_listener.arn]
      }

      target_group {
        name = aws_lb_target_group.target_group_blue.name
      }

      target_group {
        name = aws_lb_target_group.target_group_green.name
      }
    }
  }
}

์ €๋Ÿฌ๋ฉด prod_traffic_route์™€ test_traffic_route์— ๋“ค์–ด๊ฐ„ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ž๋™์œผ๋กœ ๊ต์ฒดํ•ด์ค€๋‹ค.

๊ทธ๋ฆฌ๊ณ  codepipeline์˜ deploy ์Šคํ…์„ ์ˆ˜์ •ํ•œ๋‹ค.

  stage {
    name = "Deploy"

    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "CodeDeployToECS"
      input_artifacts = ["Build"]
      version         = "1"
      run_order       = 1

      configuration = {
        ApplicationName                = aws_codedeploy_app.deploy.name
        DeploymentGroupName            = aws_codedeploy_deployment_group.deployment_group.deployment_group_name
        TaskDefinitionTemplateArtifact = "Build"
        TaskDefinitionTemplatePath     = "taskdef.json"
        AppSpecTemplateArtifact        = "Build"
        AppSpecTemplatePath            = "appspec.json"
      }
    }
  }

provider๋ฅผ ECS์—์„œ CodeDeployToECS๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , configuration์— ๋ช‡๊ฐ€์ง€ ์„ค์ •์ด ์ถ”๊ฐ€๋๋‹ค.
๊ทธ๋ž˜์„œ ์ž‘์—…์ •์˜์™€ appspec์„ ์ด์ „ ๋‹จ๊ณ„์ธ build์—์„œ ๋ฐ›์•„์™€์•ผ ํ•˜๋Š”๋ฐ... ๋‚œ ์ด๊ฑธ ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ์— ๋„ฃ๊ณ  ์‹ถ์ง€ ์•Š์•˜๋‹ค.

๊ทธ๋ž˜์„œ terraform์— local ๋ณ€์ˆ˜๋กœ ๋•Œ๋ ค๋ฐ•๊ณ 

codebuild ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ๋„ฃ์–ด์„œ

buildspec์—์„œ emitํ•ด์ฃผ๊ฒŒ ํ–ˆ๋‹ค.

๋‚ด๊ฐ€ ์‚ฌ์šฉํ•œ buildspec ์ „์ฒด ์ฝ”๋“œ๋‹ค.

version: 0.2

phases:
  pre_build:
    commands:
      - echo $TaskDefinition > taskdef.json
      - echo $AppSpec > appspec.json
      - printf '{"imageURI":"%s"}' "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG" > imageDetail.json
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$CODEBUILD_BUILD_NUMBER 
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$CODEBUILD_BUILD_NUMBER
artifacts:
  files: 
    - 'appspec.json'
    - 'taskdef.json'
    - 'imageDetail.json'
secondary-artifacts:
  DefinitionArtifact:
    files:
      - appspec.yaml
      - taskdef.json
      - imageDetail.json

appspec์˜ ๊ฒฝ์šฐ์—๋Š” ์›ํ•œ๋‹ค๋ฉด hooks์„ ์‚ฌ์šฉํ•ด์„œ ๋” ๋งŽ์€ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ•ด์„œ, ์ตœ์ดˆ๋กœ ๋ฐฐํฌ๋ฅผ ํ•ด์„œ ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ์— ๋„๋ฉ”์ธ๋„ ๋ถ™์ด๋ฉด

์ด๋ ‡๊ฒŒ ์ž˜ ๋œฐ ๊ฒƒ์ด๊ณ 

์ถ”๊ฐ€๋กœ ๋˜ ๋ฐฐํฌ ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ๊ฑธ๋ฉด

๋Œ€์ƒ๊ทธ๋ฃน๋„ ์ž๋™์œผ๋กœ ๋ฐ”๋€Œ๊ณ 

ํ•œ๋ฒˆ์— ํŠธ๋ž˜ํ”ฝ ์ „ํ™˜ํ•˜๋ฉด์„œ

์ž˜ ๋ฐ”๋€” ๊ฒƒ์ด๋‹ค.

์ด๊ฑธ ์ž˜ ์‘์šฉํ•˜๋ฉด carary ๋ฐฐํฌ ๊ฐ™์€ ๊ฒƒ๋„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.



์ฐธ์กฐ
https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/create-blue-green.html
https://docs.aws.amazon.com/ko_kr/AWSCloudFormation/latest/UserGuide/blue-green.html
https://faun.pub/aws-ecs-blue-green-deployment-setup-using-terraform-b56bb4f656ea
https://letsmake.cloud/bluegreen-fargate
https://repost.aws/questions/QUGByFL5XzT26SA7lTCz3htQ/questions/QUGByFL5XzT26SA7lTCz3htQ/codepipeline-codedeploy-blue-green-ecs-insufficient-permissions?
https://arnav40.medium.com/deploy-nodejs-on-ecs-fargate-using-terraform-with-support-for-blue-geen-deployment-using-codedeploy-a750c6214153