본문 바로가기
카테고리 없음

Terraform을 활용한 Blue/Green EKS 업그레이드

by james_janghun 2025. 4. 2.
해당 워크샵을 기준으로 작성했으며, 이 블로그의 내용도 참조해 작성합니다.

 

 

쿠버네티스는 1년에 약 3개의 마이너 버전을 업그레이드 합니다.

클러스터 업그레이드는 사실 엄청 큰 이벤트인데요. 단순히 버튼을 눌러 업그레이드가 가능하지만 쿠버네티스 버전 마다 api 변경사항이나 다양한 플러그인 도입 등 여러가지 변경사항에 대해서 대응해야합니다.

 

이번 과제에서는 EKS 클러스터 업그레이드시 블루/그린 배포 전략으로 진행해 보도록 하겠습니다.

 

EKS 배포전략

1. EKS 버전에 따른 호환성 검토

이런 것들은 공식 docs에 기본적으로 제공되는 내용이므로 반드시 꼼꼼하게 확인을 해보고 업그레이드를 진행해야합니다.

 

- 커널 버전을 확인

    ㄴ 예를 들어 user namespace 사용 시 커널 6.5 이상 필요하다는 조건이 있는 경우

- containerd 버전 확인

- CNI 요구 커널 버전 확인

- CSI 요구 버전 확인

- 각종 Controller 요구 내용 확인

- 애플리케이션 요구 사항 검토

 

2. 업그레이드 방법 결정

in-place 방식 vs blue-green 방식

 

* in-place 방식은 순차적으로 업그레이드 버튼을 통해서 직접적으로 업그레이드 하는 방식을 말합니다.

 

 

Blue/Green 배포는 무엇인가?

기본적으로 blue/green 배포는 blue(v1)라는 구형버전과 green(v2)이라는 신형버전으로 두 가지 클러스터를 존재하게 한 후 blue에서 green으로 트래픽을 이동시키는 것을 말합니다. 따라서 이 방식을 통해 실제 트래픽에 영향을 주지 않고 새 버전을 테스트할 수 있으며, 문제가되면 언제든 트래픽만 다시 이전 버전으로 돌리면 되기때문에 롤백이 아주 간단하고 빠릅니다.

 

이런 장점에도 사용이 쉽지 않은 이유는 일시적으로 동일 규모의 클러스터가 2개가 되기 때문에 가격과 규모가 매우 커지는 단점이 있습니다.

 

왜 Blue/Green을 써야하는가?

- EKS 클러스터는 다운그레이드가 불가능함(롤백보장)

기본적으로 EKS의 인플레이스(In-place) 방식을 이용하게 되면 다운그레이드가 불가능하기 때문에 롤백이 불가능합니다. 그래서 blue/green을 사용하면 롤백이 아주 간단하게 보장됩니다.

 

- API 변경이 많음

버전마다 api 변경이 잦기 때문에 호환성 문제를 만날 수 있습니다. 그렇기 때문에 반드시 잘 체크하고 검토할 시간이 필요합니다.

 

- 한 번에 여러 버전 업그레이드가 가능

EKS는 버전을 1개씩만 올릴 수 있습니다. 1.27에서 1.29로 바로 건너 뛸 수 없습니다. 하지만 Green의 신규 클러스터를 만들때는 바로 건너 뛸 수 있습니다. 다만 이는 API 변경이 많을때에 치명적이므로 버전별 업그레이드 문서를 반드시 읽어야합니다.

 

 

아키텍처 구성

이 아키텍처 구성에서는 가장 중요한 라우팅 구성과 클러스터 구성을 어떻게 하는지에 대해서 간략하게 설명하고 추후 실습에서 본격적으로 살펴보겠습니다.

 

아키텍처 개요는 다음 그림과 같습니다.

동일한 VPC 내에 AWS LoadBalancer Controller와 ExternalDNS 애드온을 통해서 두 개의 클러스터를 공유할 수 있는 환경을 만들고, 각각 두개의 클러스터를 배포합니다.

https://aws.amazon.com/ko/blogs/containers/blue-green-or-canary-amazon-eks-clusters-migration-for-stateless-argocd-workloads/

 

 

해당 아키텍처를 완성하기 위해서는 로드밸런서가 필수적입니다. 모든 트래픽은 로드밸런서를 통해서 외부에 노출되고, 해당 로드밸런서는 blue/green에서 동일한 도메인에 연결되어 가중치를 통한 트래픽 제어를 진행합니다.

 

따라서 ingress 룰이 동일하게 들어가고 route53에서는 가중치 기반 라우팅이 가능하도록 설정되어야합니다.

 

 

1. 로드 밸런서와 트래픽 제어

AWS 환경에서는 Application Load Balancer(ALB)를 사용해 두 클러스터 간의 트래픽을 제어합니다. 각 클러스터는 별도의 타겟 그룹에 연결되며, 로드 밸런서는 가중치 기반으로 트래픽을 분배합니다.

# 로드 밸런서 생성
resource "aws_lb" "main" {
  name               = "${var.app_name}-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.lb_sg.id]
  subnets            = module.vpc.public_subnets

  tags = var.tags
}

# 블루 클러스터 타겟 그룹
resource "aws_lb_target_group" "blue" {
  name     = "${var.app_name}-blue-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = module.vpc.vpc_id
  
  # 상태 체크 설정
  health_check {
    path                = "/"
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 3
    interval            = 30
    matcher             = "200"
  }
}

# 그린 클러스터 타겟 그룹
resource "aws_lb_target_group" "green" {
  name     = "${var.app_name}-green-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = module.vpc.vpc_id
  
  # 동일한 상태 체크 설정
  health_check {
    path                = "/"
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 3
    interval            = 30
    matcher             = "200"
  }
}

 

2. 트래픽 분배 설정

트래픽 분배는 로드 밸런서의 리스너 규칙을 통해 제어합니다. 초기에는 블루 클러스터로 100% 트래픽을 보내다가 점진적으로 그린 클러스터로 이동시킵니다. 이렇게 설정하면 var.blue_weightvar.green_weight 변수만 조정하여 트래픽 비율을 쉽게 변경할 수 있습니다.

# 가중치 기반 리스너 룰 설정
resource "aws_lb_listener_rule" "weighted" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 100

  action {
    type = "forward"
    forward {
      target_group {
        arn    = aws_lb_target_group.blue.arn
        weight = var.blue_weight  # 처음에는 100으로 설정
      }

      target_group {
        arn    = aws_lb_target_group.green.arn
        weight = var.green_weight  # 처음에는 0으로 설정
      }

      # 사용자 세션 유지를 위한 스티키 설정
      stickiness {
        enabled  = true
        duration = 600  # 10분
      }
    }
  }

  condition {
    path_pattern {
      values = ["/*"]
    }
  }
}

 

3. EKS 클러스터 구성

이제 실제 EKS 클러스터를 생성합니다. 블루와 그린 클러스터는 기본적으로 동일한 구성을 가지지만, Kubernetes 버전이 다릅니다.

# 블루 클러스터 (현재 운영 중인 클러스터)
module "eks_blue" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 18.0"

  cluster_name    = var.blue_cluster_name
  cluster_version = var.blue_cluster_version  # 예: "1.27"

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  # 노드 그룹 설정
  eks_managed_node_groups = {
    main = {
      desired_size = var.node_group_desired_size
      min_size     = var.node_group_min_size
      max_size     = var.node_group_max_size
      instance_types = var.node_group_instance_types
    }
  }
}

# 그린 클러스터 (새 버전으로 업그레이드된 클러스터)
module "eks_green" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 18.0"

  cluster_name    = var.green_cluster_name
  cluster_version = var.green_cluster_version  # 예: "1.29" (업그레이드 버전)

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  # 동일한 노드 그룹 설정
  eks_managed_node_groups = {
    main = {
      desired_size = var.node_group_desired_size
      min_size     = var.node_group_min_size
      max_size     = var.node_group_max_size
      instance_types = var.node_group_instance_types
    }
  }
}

 

 

 

실습하기

일단 이 워크샵을 개인 계정에서 실습하기 위한 방법을 알려드리겠습니다.

1. Git 리포지토리 클론

git clone https://github.com/aws-ia/terraform-aws-eks-blueprints.git
cd terraform-aws-eks-blueprints/patterns/blue-green-upgrade/

 

2. 환경변수 업로드

해당 CLI를 통해서 terraform.tfvars의 환경변수 파일을 우리가 사용할 폴더에 모두 링크시키도록 합니다.

그리고 최종적으로 terraform.tfvars를 수정합니다.

cp terraform.tfvars.example terraform.tfvars
ln -s ../terraform.tfvars environment/terraform.tfvars
ln -s ../terraform.tfvars eks-blue/terraform.tfvars
ln -s ../terraform.tfvars eks-green/terraform.tfvars

 

3. terraform.tfvars 수정

반드시 수정해 둘것은 다음과 같습니다.

hosted_zone_name : Route53에 있는 개인 도메인을 입력해서 넣어야 합니다.

eks_admin_role_name : eks롤의 이름을 지정합니다. role이 없다면 만들어야 합니다.

 

SSH public key 등록

aws_secret_manager를 통해서 github-blueprint-ssh-key에 본인이 사용하는 ssh키를 넣어두기 바랍니다.

github에도 마찬가지 해당 public key를 넣어둡니다.

 

SSH and GPGkeys 탭에 들어가서 New SSH key 버튼을 통해서 생성할 수 있습니다.

 

 

4. terraform을 이용한 실습환경 설치

테라폼을 이용해서 다음과 같이 3가지를 설치합니다.

 

VPC를 포함한 네트워크 구성 및 기본 구성

cd environment
terraform init
terraform apply

 

모듈 구성

사실 모듈이 아주 핵심 코드입니다.

Argocd, ecr, 각종 리소스의 request, 네트워크 정보, 쿠버네티스 리소스 정보 등을 모두 이곳에서 통제합니다.

# Required for public ECR where Karpenter artifacts are hosted
provider "aws" {
  region = "us-east-1"
  alias  = "ecr"
}

locals {
  environment = var.environment_name
  service     = var.service_name
  region      = var.aws_region

  env  = local.service
  name = "${local.environment}-${local.service}"

  # Mapping
  hosted_zone_name                            = var.hosted_zone_name
  ingress_type                                = var.ingress_type
  aws_secret_manager_git_private_ssh_key_name = var.aws_secret_manager_git_private_ssh_key_name
  cluster_version                             = var.cluster_version
  argocd_secret_manager_name                  = var.argocd_secret_manager_name_suffix
  eks_admin_role_name                         = var.eks_admin_role_name

  gitops_workloads_url      = "${var.gitops_workloads_org}/${var.gitops_workloads_repo}"
  gitops_workloads_path     = var.gitops_workloads_path
  gitops_workloads_revision = var.gitops_workloads_revision

  gitops_addons_url      = "${var.gitops_addons_org}/${var.gitops_addons_repo}"
  gitops_addons_basepath = var.gitops_addons_basepath
  gitops_addons_path     = var.gitops_addons_path
  gitops_addons_revision = var.gitops_addons_revision

  # Route 53 Ingress Weights
  argocd_route53_weight      = var.argocd_route53_weight
  route53_weight             = var.route53_weight
  ecsfrontend_route53_weight = var.ecsfrontend_route53_weight

  eks_cluster_domain = "${local.environment}.${local.hosted_zone_name}" # for external-dns

  tag_val_vpc            = local.environment
  tag_val_public_subnet  = "${local.environment}-public-"
  tag_val_private_subnet = "${local.environment}-private-"

  node_group_name = "managed-ondemand"

  #---------------------------------------------------------------
  # ARGOCD ADD-ON APPLICATION
  #---------------------------------------------------------------

  aws_addons = {
    enable_cert_manager          = true
    enable_aws_ebs_csi_resources = true # generate gp2 and gp3 storage classes for ebs-csi
    #enable_aws_efs_csi_driver                    = true
    #enable_aws_fsx_csi_driver                    = true
    enable_aws_cloudwatch_metrics = true
    #enable_aws_privateca_issuer                  = true
    #enable_cluster_autoscaler                    = true
    enable_external_dns                 = true
    enable_external_secrets             = true
    enable_aws_load_balancer_controller = true
    #enable_fargate_fluentbit                     = true
    enable_aws_for_fluentbit = true
    #enable_aws_node_termination_handler          = true
    enable_karpenter = true
    #enable_velero                                = true
    #enable_aws_gateway_api_controller            = true
    #enable_aws_secrets_store_csi_driver_provider = true
  }
  oss_addons = {
    #enable_argo_rollouts                         = true
    #enable_argo_workflows                        = true
    #enable_cluster_proportional_autoscaler       = true
    #enable_gatekeeper                            = true
    #enable_gpu_operator                          = true
    enable_ingress_nginx = true
    enable_kyverno       = true
    #enable_kube_prometheus_stack                 = true
    enable_metrics_server = true
    #enable_prometheus_adapter                    = true
    #enable_secrets_store_csi_driver              = true
    #enable_vpa                                   = true
    #enable_foo                                   = true # you can add any addon here, make sure to update the gitops repo with the corresponding application set
  }
  addons = merge(local.aws_addons, local.oss_addons, { kubernetes_version = local.cluster_version })

  #----------------------------------------------------------------
  # GitOps Bridge, define metadatas to pass from Terraform to ArgoCD
  #----------------------------------------------------------------

  addons_metadata = merge(
    try(module.eks_blueprints_addons.gitops_metadata, {}), # eks blueprints addons automatically expose metadatas
    {
      aws_cluster_name = module.eks.cluster_name
      aws_region       = local.region
      aws_account_id   = data.aws_caller_identity.current.account_id
      aws_vpc_id       = data.aws_vpc.vpc.id
      cluster_endpoint = try(module.eks.cluster_endpoint, {})
      env              = local.env
    },
    {
      argocd_password                             = bcrypt(data.aws_secretsmanager_secret_version.admin_password_version.secret_string)
      aws_secret_manager_git_private_ssh_key_name = local.aws_secret_manager_git_private_ssh_key_name

      gitops_workloads_url      = local.gitops_workloads_url
      gitops_workloads_path     = local.gitops_workloads_path
      gitops_workloads_revision = local.gitops_workloads_revision

      addons_repo_url      = local.gitops_addons_url
      addons_repo_basepath = local.gitops_addons_basepath
      addons_repo_path     = local.gitops_addons_path
      addons_repo_revision = local.gitops_addons_revision
    },
    {
      eks_cluster_domain         = local.eks_cluster_domain
      external_dns_policy        = "sync"
      ingress_type               = local.ingress_type
      argocd_route53_weight      = local.argocd_route53_weight
      route53_weight             = local.route53_weight
      ecsfrontend_route53_weight = local.ecsfrontend_route53_weight
      #target_group_arn = local.service == "blue" ? data.aws_lb_target_group.tg_blue.arn : data.aws_lb_target_group.tg_green.arn # <-- Add this line
      #      external_lb_dns = data.aws_lb.alb.dns_name
    }
  )

  #---------------------------------------------------------------
  # Manifests for bootstraping the cluster for addons & workloads
  #---------------------------------------------------------------

  argocd_apps = {
    addons    = file("${path.module}/../../bootstrap/addons.yaml")
    workloads = file("${path.module}/../../bootstrap/workloads.yaml")
  }


  tags = {
    Blueprint  = local.name
    GithubRepo = "github.com/aws-ia/terraform-aws-eks-blueprints"
  }

}

# Find the user currently in use by AWS
data "aws_caller_identity" "current" {}

data "aws_vpc" "vpc" {
  filter {
    name   = "tag:Name"
    values = [local.tag_val_vpc]
  }
}

data "aws_subnets" "private" {
  filter {
    name   = "tag:Name"
    values = ["${local.tag_val_private_subnet}*"]
  }
}

#Add Tags for the new cluster in the VPC Subnets
resource "aws_ec2_tag" "private_subnets" {
  for_each    = toset(data.aws_subnets.private.ids)
  resource_id = each.value
  key         = "kubernetes.io/cluster/${local.environment}-${local.service}"
  value       = "shared"
}

data "aws_subnets" "public" {
  filter {
    name   = "tag:Name"
    values = ["${local.tag_val_public_subnet}*"]
  }
}

#Add Tags for the new cluster in the VPC Subnets
resource "aws_ec2_tag" "public_subnets" {
  for_each    = toset(data.aws_subnets.public.ids)
  resource_id = each.value
  key         = "kubernetes.io/cluster/${local.environment}-${local.service}"
  value       = "shared"
}

# Get HostedZone four our deployment
data "aws_route53_zone" "sub" {
  name = "${local.environment}.${local.hosted_zone_name}"
}

################################################################################
# AWS Secret Manager for argocd password
################################################################################

data "aws_secretsmanager_secret" "argocd" {
  name = "${local.argocd_secret_manager_name}.${local.environment}"
}

data "aws_secretsmanager_secret_version" "admin_password_version" {
  secret_id = data.aws_secretsmanager_secret.argocd.id
}

################################################################################
# EKS Cluster
################################################################################
#tfsec:ignore:aws-eks-enable-control-plane-logging
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.15.2"

  cluster_name                   = local.name
  cluster_version                = local.cluster_version
  cluster_endpoint_public_access = true

  vpc_id     = data.aws_vpc.vpc.id
  subnet_ids = data.aws_subnets.private.ids

  #we uses only 1 security group to allow connection with Fargate, MNG, and Karpenter nodes
  create_node_security_group = false
  eks_managed_node_groups = {
    initial = {
      node_group_name = local.node_group_name
      instance_types  = ["m5.large"]

      min_size     = 1
      max_size     = 5
      desired_size = 3
      subnet_ids   = data.aws_subnets.private.ids
    }
  }

  manage_aws_auth_configmap = true
  aws_auth_roles = concat(
    [for team in module.eks_blueprints_dev_teams : team.aws_auth_configmap_role],
    [
      module.eks_blueprints_platform_teams.aws_auth_configmap_role,
      {
        rolearn  = module.eks_blueprints_addons.karpenter.node_iam_role_arn
        username = "system:node:{{EC2PrivateDNSName}}"
        groups = [
          "system:bootstrappers",
          "system:nodes",
        ]
      },
      {
        rolearn  = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.eks_admin_role_name}" # The ARN of the IAM role
        username = "ops-role"                                                                                      # The user name within Kubernetes to map to the IAM role
        groups   = ["system:masters"]                                                                              # A list of groups within Kubernetes to which the role is mapped; Checkout K8s Role and Rolebindings
      }
    ]
  )

  tags = merge(local.tags, {
    # NOTE - if creating multiple security groups with this module, only tag the
    # security group that Karpenter should utilize with the following tag
    # (i.e. - at most, only one security group should have this tag in your account)
    "karpenter.sh/discovery" = "${local.environment}-${local.service}"
  })
}

data "aws_iam_role" "eks_admin_role_name" {
  count = local.eks_admin_role_name != "" ? 1 : 0
  name  = local.eks_admin_role_name
}

################################################################################
# EKS Blueprints Teams
################################################################################
module "eks_blueprints_platform_teams" {
  source  = "aws-ia/eks-blueprints-teams/aws"
  version = "~> 1.0"

  name = "team-platform"

  # Enables elevated, admin privileges for this team
  enable_admin = true

  # Define who can impersonate the team-platform Role
  users = [
    data.aws_caller_identity.current.arn,
    try(data.aws_iam_role.eks_admin_role_name[0].arn, data.aws_caller_identity.current.arn),
  ]
  cluster_arn       = module.eks.cluster_arn
  oidc_provider_arn = module.eks.oidc_provider_arn

  labels = {
    "elbv2.k8s.aws/pod-readiness-gate-inject" = "enabled",
    "appName"                                 = "platform-team-app",
    "projectName"                             = "project-platform",
    #"pod-security.kubernetes.io/enforce"      = "restricted",
  }

  annotations = {
    team = "platform"
  }

  namespaces = {
    "team-platform" = {

      resource_quota = {
        hard = {
          "requests.cpu"    = "10000m",
          "requests.memory" = "20Gi",
          "limits.cpu"      = "20000m",
          "limits.memory"   = "50Gi",
          "pods"            = "20",
          "secrets"         = "20",
          "services"        = "20"
        }
      }

      limit_range = {
        limit = [
          {
            type = "Pod"
            max = {
              cpu    = "1000m"
              memory = "1Gi"
            },
            min = {
              cpu    = "10m"
              memory = "4Mi"
            }
          },
          {
            type = "PersistentVolumeClaim"
            min = {
              storage = "24M"
            }
          }
        ]
      }

    }

  }

  tags = local.tags
}

module "eks_blueprints_dev_teams" {
  source  = "aws-ia/eks-blueprints-teams/aws"
  version = "~> 1.0"

  for_each = {
    burnham = {
      labels = {
        "elbv2.k8s.aws/pod-readiness-gate-inject" = "enabled",
        "appName"                                 = "burnham-team-app",
        "projectName"                             = "project-burnham",
        #"pod-security.kubernetes.io/enforce"      = "restricted",
      }
    }
    riker = {
      labels = {
        "elbv2.k8s.aws/pod-readiness-gate-inject" = "enabled",
        "appName"                                 = "riker-team-app",
        "projectName"                             = "project-riker",
      }
    }
  }
  name = "team-${each.key}"

  users             = [data.aws_caller_identity.current.arn]
  cluster_arn       = module.eks.cluster_arn
  oidc_provider_arn = module.eks.oidc_provider_arn

  labels = merge(
    {
      team = each.key
    },
    try(each.value.labels, {})
  )

  annotations = {
    team = each.key
  }

  namespaces = {
    "team-${each.key}" = {
      labels = merge(
        {
          team = each.key
        },
        try(each.value.labels, {})
      )

      resource_quota = {
        hard = {
          "requests.cpu"    = "100",
          "requests.memory" = "20Gi",
          "limits.cpu"      = "200",
          "limits.memory"   = "50Gi",
          "pods"            = "100",
          "secrets"         = "10",
          "services"        = "20"
        }
      }

      limit_range = {
        limit = [
          {
            type = "Pod"
            max = {
              cpu    = "2"
              memory = "1Gi"
            }
            min = {
              cpu    = "10m"
              memory = "4Mi"
            }
          },
          {
            type = "PersistentVolumeClaim"
            min = {
              storage = "24M"
            }
          },
          {
            type = "Container"
            default = {
              cpu    = "50m"
              memory = "24Mi"
            }
          }
        ]
      }
    }
  }

  tags = local.tags

}

module "eks_blueprints_ecsdemo_teams" {
  source  = "aws-ia/eks-blueprints-teams/aws"
  version = "~> 1.0"

  for_each = {
    ecsdemo-frontend = {}
    ecsdemo-nodejs   = {}
    ecsdemo-crystal  = {}
  }
  name = "team-${each.key}"

  users             = [data.aws_caller_identity.current.arn]
  cluster_arn       = module.eks.cluster_arn
  oidc_provider_arn = module.eks.oidc_provider_arn

  labels = {
    "elbv2.k8s.aws/pod-readiness-gate-inject" = "enabled",
    "appName"                                 = "${each.key}-app",
    "projectName"                             = each.key,
    "environment"                             = "dev",
  }

  annotations = {
    team = each.key
  }

  namespaces = {
    (each.key) = {
      labels = {
        "elbv2.k8s.aws/pod-readiness-gate-inject" = "enabled",
        "appName"                                 = "${each.key}-app",
        "projectName"                             = each.key,
        "environment"                             = "dev",
      }

      resource_quota = {
        hard = {
          "requests.cpu"    = "100",
          "requests.memory" = "20Gi",
          "limits.cpu"      = "200",
          "limits.memory"   = "50Gi",
          "pods"            = "100",
          "secrets"         = "10",
          "services"        = "20"
        }
      }

      limit_range = {
        limit = [
          {
            type = "Pod"
            max = {
              cpu    = "2"
              memory = "1Gi"
            }
            min = {
              cpu    = "10m"
              memory = "4Mi"
            }
          },
          {
            type = "PersistentVolumeClaim"
            min = {
              storage = "24M"
            }
          },
          {
            type = "Container"
            default = {
              cpu    = "50m"
              memory = "24Mi"
            }
          }
        ]
      }
    }
  }

  tags = local.tags
}

################################################################################
# GitOps Bridge: Private ssh keys for git
################################################################################
data "aws_secretsmanager_secret" "workload_repo_secret" {
  name = local.aws_secret_manager_git_private_ssh_key_name
}

data "aws_secretsmanager_secret_version" "workload_repo_secret" {
  secret_id = data.aws_secretsmanager_secret.workload_repo_secret.id
}

resource "kubernetes_namespace" "argocd" {
  depends_on = [module.eks_blueprints_addons]
  metadata {
    name = "argocd"
  }
}

resource "kubernetes_secret" "git_secrets" {

  for_each = {
    git-addons = {
      type = "git"
      url  = local.gitops_addons_url
      # comment if you want to uses public repo wigh syntax "https://github.com/xxx" syntax, uncomment when using syntax "git@github.com:xxx"
      sshPrivateKey = data.aws_secretsmanager_secret_version.workload_repo_secret.secret_string
    }
    git-workloads = {
      type = "git"
      url  = local.gitops_workloads_url
      # comment if you want to uses public repo wigh syntax "https://github.com/xxx" syntax, uncomment when using syntax "git@github.com:xxx"
      sshPrivateKey = data.aws_secretsmanager_secret_version.workload_repo_secret.secret_string
    }
  }
  metadata {
    name      = each.key
    namespace = kubernetes_namespace.argocd.metadata[0].name
    labels = {
      "argocd.argoproj.io/secret-type" = "repo-creds"
    }
  }
  data = each.value
}

################################################################################
# GitOps Bridge: Bootstrap
################################################################################
module "gitops_bridge_bootstrap" {
  source = "github.com/gitops-bridge-dev/gitops-bridge-argocd-bootstrap-terraform?ref=v2.0.0"

  cluster = {
    cluster_name = module.eks.cluster_name
    environment  = local.environment
    metadata     = local.addons_metadata
    addons       = local.addons
  }
  apps = local.argocd_apps

  argocd = {
    create_namespace = false
    set = [
      {
        name  = "server.service.type"
        value = "LoadBalancer"
      }
    ]
    set_sensitive = [
      {
        name  = "configs.secret.argocdServerAdminPassword"
        value = bcrypt(data.aws_secretsmanager_secret_version.admin_password_version.secret_string)
      }
    ]
  }

  depends_on = [kubernetes_secret.git_secrets]
}

################################################################################
# EKS Blueprints Addons
################################################################################
module "eks_blueprints_addons" {
  source = "aws-ia/eks-blueprints-addons/aws"

  cluster_name      = module.eks.cluster_name
  cluster_endpoint  = module.eks.cluster_endpoint
  cluster_version   = module.eks.cluster_version
  oidc_provider_arn = module.eks.oidc_provider_arn

  # Using GitOps Bridge
  create_kubernetes_resources = false

  eks_addons = {

    # Remove for workshop as ebs-csi is long to provision (15mn)
    # aws-ebs-csi-driver = {
    #   most_recent              = true
    #   service_account_role_arn = module.ebs_csi_driver_irsa.iam_role_arn
    # }
    coredns = {
      most_recent = true
    }
    vpc-cni = {
      # Specify the VPC CNI addon should be deployed before compute to ensure
      # the addon is configured before data plane compute resources are created
      # See README for further details
      service_account_role_arn = module.vpc_cni_irsa.iam_role_arn
      before_compute           = true
      #addon_version  = "v1.12.2-eksbuild.1"
      most_recent = true # To ensure access to the latest settings provided
      configuration_values = jsonencode({
        env = {
          # Reference docs https://docs.aws.amazon.com/eks/latest/userguide/cni-increase-ip-addresses.html
          ENABLE_PREFIX_DELEGATION = "true"
          WARM_PREFIX_TARGET       = "1"
        }
      })
    }
    kube-proxy = {
      most_recent = true
    }
  }

  # EKS Blueprints Addons
  enable_cert_manager = try(local.aws_addons.enable_cert_manager, false)
  #enable_aws_ebs_csi_resources  = try(local.aws_addons.enable_aws_ebs_csi_resources, false)
  enable_aws_efs_csi_driver           = try(local.aws_addons.enable_aws_efs_csi_driver, false)
  enable_aws_fsx_csi_driver           = try(local.aws_addons.enable_aws_fsx_csi_driver, false)
  enable_aws_cloudwatch_metrics       = try(local.aws_addons.enable_aws_cloudwatch_metrics, false)
  enable_aws_privateca_issuer         = try(local.aws_addons.enable_aws_privateca_issuer, false)
  enable_cluster_autoscaler           = try(local.aws_addons.enable_cluster_autoscaler, false)
  enable_external_dns                 = try(local.aws_addons.enable_external_dns, false)
  external_dns_route53_zone_arns      = [data.aws_route53_zone.sub.arn]
  enable_external_secrets             = try(local.aws_addons.enable_external_secrets, false)
  enable_aws_load_balancer_controller = try(local.aws_addons.enable_aws_load_balancer_controller, false)
  aws_load_balancer_controller = {
    service_account_name = "aws-lb-sa"
  }

  enable_fargate_fluentbit              = try(local.aws_addons.enable_fargate_fluentbit, false)
  enable_aws_for_fluentbit              = try(local.aws_addons.enable_aws_for_fluentbit, false)
  enable_aws_node_termination_handler   = try(local.aws_addons.enable_aws_node_termination_handler, false)
  aws_node_termination_handler_asg_arns = [for asg in module.eks.self_managed_node_groups : asg.autoscaling_group_arn]
  enable_karpenter                      = try(local.aws_addons.enable_karpenter, false)
  enable_velero                         = try(local.aws_addons.enable_velero, false)
  #velero = {
  #  s3_backup_location = "${module.velero_backup_s3_bucket.s3_bucket_arn}/backups"
  #}
  enable_aws_gateway_api_controller = try(local.aws_addons.enable_aws_gateway_api_controller, false)
  #enable_aws_secrets_store_csi_driver_provider = try(local.enable_aws_secrets_store_csi_driver_provider, false)

  tags = local.tags
}

module "ebs_csi_driver_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.20"

  role_name_prefix = "${module.eks.cluster_name}-ebs-csi-"

  attach_ebs_csi_policy = true

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"]
    }
  }

  tags = local.tags
}

module "vpc_cni_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.20"

  role_name_prefix = "${module.eks.cluster_name}-vpc-cni-"

  attach_vpc_cni_policy = true
  vpc_cni_enable_ipv4   = true

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:aws-node"]
    }
  }

  tags = local.tags
}

 

 

EKS blue 클러스터

cd eks-blue
terraform init
terraform apply

 

providers.tf

aws,kubernetes, helm 프로바이더 및 버전을 지정합니다.

terraform {
  required_version = ">= 1.4.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = ">= 2.20.0"
    }
    helm = {
      source  = "hashicorp/helm"
      version = ">= 2.9.0"
    }
  }
}

 

main.tf

kubernetes와 helm 프로바이더를 정의하고 eks 클러스터를 정의합니다. 이때 가중치 설정값도 여기서 진행하도록 설정합니다.

provider "aws" {
  region = var.aws_region
}

provider "kubernetes" {
  host                   = module.eks_cluster.eks_cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks_cluster.cluster_certificate_authority_data)

  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args        = ["eks", "get-token", "--cluster-name", module.eks_cluster.eks_cluster_id]
  }
}

provider "helm" {
  kubernetes {
    host                   = module.eks_cluster.eks_cluster_endpoint
    cluster_ca_certificate = base64decode(module.eks_cluster.cluster_certificate_authority_data)

    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "aws"
      args        = ["eks", "get-token", "--cluster-name", module.eks_cluster.eks_cluster_id]
    }
  }
}

module "eks_cluster" {
  source = "../modules/eks_cluster"

  aws_region      = var.aws_region
  service_name    = "blue"
  cluster_version = "1.26"

  argocd_route53_weight      = "100"
  route53_weight             = "100"
  ecsfrontend_route53_weight = "100"

  environment_name    = var.environment_name
  hosted_zone_name    = var.hosted_zone_name
  eks_admin_role_name = var.eks_admin_role_name

  aws_secret_manager_git_private_ssh_key_name = var.aws_secret_manager_git_private_ssh_key_name
  argocd_secret_manager_name_suffix           = var.argocd_secret_manager_name_suffix
  ingress_type                                = var.ingress_type

  gitops_addons_org      = var.gitops_addons_org
  gitops_addons_repo     = var.gitops_addons_repo
  gitops_addons_basepath = var.gitops_addons_basepath
  gitops_addons_path     = var.gitops_addons_path
  gitops_addons_revision = var.gitops_addons_revision

  gitops_workloads_org      = var.gitops_workloads_org
  gitops_workloads_repo     = var.gitops_workloads_repo
  gitops_workloads_revision = var.gitops_workloads_revision
  gitops_workloads_path     = var.gitops_workloads_path

}

 

 

EKS Green 클러스터

cd eks-green
terraform init
terraform apply

 

main.tf

provider "aws" {
  region = var.aws_region
}

provider "kubernetes" {
  host                   = module.eks_cluster.eks_cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks_cluster.cluster_certificate_authority_data)

  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args        = ["eks", "get-token", "--cluster-name", module.eks_cluster.eks_cluster_id]
  }
}

provider "helm" {
  kubernetes {
    host                   = module.eks_cluster.eks_cluster_endpoint
    cluster_ca_certificate = base64decode(module.eks_cluster.cluster_certificate_authority_data)

    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "aws"
      args        = ["eks", "get-token", "--cluster-name", module.eks_cluster.eks_cluster_id]
    }
  }
}

module "eks_cluster" {
  source = "../modules/eks_cluster"

  aws_region      = var.aws_region
  service_name    = "green"
  cluster_version = "1.28" # Here, we deploy the cluster with the N+1 Kubernetes Version

  argocd_route53_weight      = "100" # We control with theses parameters how we send traffic to the workloads in the new cluster
  route53_weight             = "100"
  ecsfrontend_route53_weight = "0"

  environment_name    = var.environment_name
  hosted_zone_name    = var.hosted_zone_name
  eks_admin_role_name = var.eks_admin_role_name

  aws_secret_manager_git_private_ssh_key_name = var.aws_secret_manager_git_private_ssh_key_name
  argocd_secret_manager_name_suffix           = var.argocd_secret_manager_name_suffix
  ingress_type                                = var.ingress_type

  gitops_addons_org      = var.gitops_addons_org
  gitops_addons_repo     = var.gitops_addons_repo
  gitops_addons_basepath = var.gitops_addons_basepath
  gitops_addons_path     = var.gitops_addons_path
  gitops_addons_revision = var.gitops_addons_revision

  gitops_workloads_org      = var.gitops_workloads_org
  gitops_workloads_repo     = var.gitops_workloads_repo
  gitops_workloads_revision = var.gitops_workloads_revision
  gitops_workloads_path     = var.gitops_workloads_path

}

 

 

5. 콘솔 확인

이 워크샵은 굉장히 규모가 큽니다..

리소스부터 쭉 확인하고 넘어갑니다.

 

EKS 클러스터

일단 EKS 2개의 클러스터가 생성됩니다. 1.26버전, 1.27버전입니다.

 

Route53 도메인과 ACM

terraform.tfvars에 적은 도메인에 대한 서브도메인으로 추가적인 내용들이 생성됩니다.

제가 사용하는 도메인에 서브도메인을 하나 더 발급받아서 추가됩니다. ACM도 해당 도메인으로 추가발급되니 참고바랍니다.

 

또한 보면 레코드가 엄청 많습니다. 각각 애플리케이션입니다.

 

애플리케이션 확인

이 중 simple go application를 확인해봅시다.

https://burnham.eks-blueprint.devopsjames.shop/

해당 페이지는 우리가 고객에게 제공하는 서비스 페이지로 이해하면됩니다.

해당 페이지에 메인 클러스터의 상태를 보여주도록 일부러 표시하고 있으며 EKS blueprint라는 말을 볼 수 있습니다.

 

 

 

ArgoCD 접근

해당 CLI를 통해서 접속 URL과 아이디 비밀번호를 취득하고 들어가봅시다.

aws eks --region ap-northeast-2 update-kubeconfig --name eks-blueprint-blue
echo "ArgoCD URL: https://$(kubectl get svc -n argocd argo-cd-argocd-server -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')"
echo "ArgoCD Username: admin"
echo "ArgoCD Password: $(aws secretsmanager get-secret-value --secret-id argocd-admin-secret.eks-blueprint --query SecretString --output text --region ap-northeast-2)"

 

argoCD를 들어가보죠. karpenter뿐 아니라 모든 필수 워크로드들이 모두 argocd를 통해 관리되고 있습니다.

애플리케이션 양이 엄청 많습니다.

 

 

 

이 외에도 확인해보면 재밌는 애플리케이션이 많으니까 확인해보기 바랍니다.

 

 

자 다시 워크샵으로 돌아와서 클러스터 업그레이드가 주 목적이므로 클러스터 업그레이드시 CLI로 해당 내용을 확인할 수 있도록 해봅시다.

워크샵에서는 burnham 애플리케이션을 사용하라고 합니다.

kubectl get deployment -n team-burnham -l app=burnham

 

3개의 파드가 떠있는데 로그를 확인하면 클러스터 이름을 말해줍니다. 즉 이게 green으로 바뀌면 업그레이드가 되었다고 볼 수 있죠.

 

다음 명령어를 통해서 이제 지속적으로 클러스터 명만 추출하도록 하겠습니다.

$ URL=$(echo -n "https://" ; kubectl get ing -n team-burnham burnham-ingress -o json | jq ".spec.rules[0].host" -r)
$ curl -s $URL | grep CLUSTER_NAME | awk -F "<span>|</span>" '{print $4}'
eks-blueprint-blue

 

 

ExternalDNS

blue-green 배포의 핵심은 동일 도메인에 대한 라우팅 전환입니다.

이때 ExternalDNS를 활용해서 도메인 전환을 쿠버네티스에서 자동으로 할 수 있도록 구현합니다.

 

특히 ExternalDNS는 EKS에서 addon으로 제공하고 있으며 두 개의 클러스터가 같은 도메인을 바라볼 수 있도록 합니다.

main.tf에서는 해당 내용을 true로 열어서 사용할 수 있도록 했고, externalDNS 옵션에서 external_dns_policy를 sync로 하여 서비스나 인그레스 객체가 생성될때 DNS 레코드를 컨트롤 할 수 있도록 했습니다.

enable_external_dns = true
external_dns_route53_zone_arns = [data.aws_route53_zone.sub.arn]

 

이때 두개의 클러스터가 공존하기 때문에 소유자와 소유권을 지정해 대상에 대한 통제를 진행합니다.

특히 소유자 ID를 지정하여 각 컨트롤러가 자신의 클러스터에 해당하는 레코드만 수정할 수 있도록 합니다. 또한 txt 레코드를 연결해 레코드 소유자를 지정할 수 있도록 명확하게 표시합니다.

"heritage=external-dns,external-dns/owner=eks-blueprint-blue,external-dns/resource=ingress/team-burnham/burnham-ingress"

위 예시의 경우

  • 레코드 소유자: external-dns 컨트롤러
  • 소속 클러스터: eks-blueprint-blue EKS 클러스터
  • 연결된 리소스: team-burnham 네임스페이스의 burnham-ingress라는 인그레스 리소스

이를 통해서 가중치 레코드를 사용해 클러스터에 정의된 인그레스 리소스의 가중치를 변경하면서 blue -> green으로 넘어가는 로직을 구현합니다.

 

가중치 레코드 구현

ExternalDNS 에드온을 통해서 ingress 객체에 특정 어노테이션을 정의할 수 있고, ArgoCD가 워크로드를 통기화해주고 있습니다.

 

burnham 배포에 초점을 맞춰 burnham-ingress 객체를 구성합니다.

external-dns.alpha.kubernetes.io/set-identifier: {{ .Values.spec.clusterName }}
external-dns.alpha.kubernetes.io/aws-weight: '{{ .Values.spec.ingress.route53_weight }}'

 

set-identifier : 생성하는 클러스터 이름과 external-dns의 txtOwnerId 구성 정의와 일치해야합니다.

aws-weight :  가중치 레코드 값을 구성하는데 사용합니다. helm values나 테라폼으로 주입하면 됩니다.

 

Route 53 가중치 레코드 작동

실제로 라우팅은 다음과 같이 3단계로 진행합니다. 이렇게 라우팅 가중치를 변경하면서 사용자에게 자연스럽게 업그레이드 된 버전을 노출시킵니다.

eks-blue eks-green Route53 라우팅
100 0 eks-blue
50 50 eks-blue/eks-green 분산
0 100 eks-green

 

 

1단계 blue(100) / green(0)의 라우팅 구성

route53에서 가중치를 살펴보면 blue만 제공되고 있습니다.

 

 

우리는 terraform을 이용하고 burnham 애플리케이션을 기준으로 합니다. 확인을 위해서 한쪽에는 while 문을 통해서 반복시키고 가중치를 테라폼을 통해 수정하겠습니다.

while true; do
  curl -s $URL | grep CLUSTER_NAME | awk -F "<span>|</span>" '{print $4}'
  sleep 1
done

 

이런식으로 업그레이드를 확인해봅니다.

 

2단계 blue(100) / green(100)의 라우팅 구성

eks-green 디렉토리의 main.tf 파일에서 route53_weight의 값을  100으로 수정하고 terraform을 배포합니다.

 

실제로 체킹을 계속 진행하면서 해보면 green을 잠시동안 계속 라우팅을 주다가, blue로 다시 변경되었다가 green으로 변경되는 모습을 보인다.

 

 

3단계 blue(0) / green(100)의 라우팅 구성 (버전업)

https://aws-ia.github.io/terraform-aws-eks-blueprints/patterns/blue-green-upgrade/

 

자 이제 blue의 라우팅을 완전히 없애기 위해 blue의 라우팅값을 0으로 변경하겠습니다.

워크샵에서는 argocd도 운영하기 때문에 weight 0으로 변경된 로그도 확인이 가능합니다.

 

레코드 상 0으로 변경된 것을 확인할 수 있습니다.

 

동일도메인에서 순단이 하나도 없이 잘 전환되는 것을 확인할 수 있습니다.

 

다른 모든 애플리케이션들도 그냥 자동으로 변경된 라우팅을 타게됩니다. 같은 도메인이라도.

 

(참고) 테라폼으로 in-place 업그레이드

아주 쉽게 cluster_version을 1.27에서 더 높은 버전으로 바꾸면 끝난다. 다만 이때 1단계씩밖에 안됨을 명심하자.

 

예를들어 1.27에서 1.29로 바로 올릴 수는 없기 때문에 다음과 같이 에러가 발생한다.

 

따라서 1.28로 적고 업그레이드를 진행해보자.

 

바로 업그레이드를 시작한다. 사실이는 콘솔에서 누르는 것과 동일하다.

 

다만 콘솔로 업그레이드 하는 것보다 큰 장점은 다음과 같다.

- 환경변수를 잘 설정해준다면 버전 입력 후 terraform apply 만 입력하면 자동으로 addon까지 버전 업그레이드 시킬 수 있다.

 

- IaC의 장점인 코드 관리가 유리하다.

 

- Terraform 특성상 실패지점을 바로 확인할 수 있고, 어떤 리소스가 업그레이드 되는지 사전 파악이 가능하다.

 

약 7분정도 컨트롤 플레인을 먼저 업그레이드 합니다.

 

그리고 바로 addon을 업그레이드 합니다.

 

노드그룹을 업그레이드합니다.

 

약 16분 정도 후에 노드 그룹까지 업그레이드가 되었습니다.

 

특이사항 없이 1.28로 업그레이드 되었습니다.

 

(참고) EKS Cluster Insight의 활용

EKS 대시보드에서는 이와 같이 업그레이드가 필요한 항목에 대해서 아주 친절하게 제시하고 있습니다. 참고로 이 항목은 클러스터가 생성된지 약 30분 이상은 지나야 표시됩니다. 클러스터가 생성되고 진단하는데 시간이 필요하기 때문입니다.

 

예를 들어 1.27버전에서 deprecated 되는 api에 대해서도 설명해주며, 권장조치 항목과 사용 중단 세부 정보에 대해서 제공합니다.

이는 정말 관리자 입장에서 편한기능입니다.

 

또한 인사이트에서는 컨트롤 플레인에 대한 모니터링, 클러스터 상태 정보까지 제공하므로 잘보면 정말 유용한 정보들이 많습니다.