[Terraform@CloudNet] EKS on Fargate by Terraform

2024. 7. 27. 23:35EKS@Terraform

- CloudNet에서 주관하는 Terraform 스터디 내용입니다

- 내용은 위 책 테라폼으로 시작하는 IaC 를 기준으로 정리하였습니다.

- 실습은 M1 macbook air 에서 진행했습니다.

- 매번 좋은 스터디를 진행해 주시는 CloudNet 팀 감사드립니다

- 잘못된 점, 업데이트된 지식, 다른 코멘트 언제나 환영입니다!



Amazon EKS Blueprints for Terraform

AWS는 Terraform을 사용해서 EKS를 운영하는 사례를 Amazon EKS Blueprints for Terraform 에서 제공합니다. 이 중 Karpenter 적용한 EKS cluster 를 Fargate 서버리스 클러스터에서 올리는 실습을 해보려고 합니다.

 

실습을 시작하기 전 여기에서 적용한 서비스와 개념들을 하나씩 간단하게 소개해보겠습니다

 

 

 

Karpenter

karpenter는 k8s native한 노드 수명 주기 관리 솔루션(오토스케일러) 입니다. 카펜터 말고도 기존에 사용되던 오토스케일러(HPA, VPA) 가 있었지만 새 리소스 생성 및 반영에 상대적으로 긴 시간(2-3분) 이 걸렸는데, 카펜터 실습에서는 리소스 변경이 수 초 안에  반영되어 매우 놀랐던 기억이 있습니다. 트래픽 급증에 보다 순발력 있게 대응할 수 있을 것 같습니다.

 

지난번 EKS 스터디에서 Karpenter 활용 실습을 진행한 내용이 있으니 자세한 내용이 필요하다면 이쪽으로..ㅎㅎ

 

[EKS@CloudNet] EKS Autoscaling: Karpenter 외

- CloudNet에서 주관하는 EKS 스터디 2기 내용입니다 - 매번 좋은 스터디를 진행해 주시는 CloudNet 팀 감사드립니다 Autoscaling이란? 개인적으로 오토스케일링이 클라우드 컴퓨팅의 가장 큰 존재 이유라

hitherex.tistory.com

 

 

 

 

Fargate

AWS가 제공하는 서버리스 환경입니다. VM수준의 격리 기능을 제공하며, 컨테이너 이미지 빌드 후 적절한 리소스를 생성해 줍니다.

https://aws.amazon.com/ko/fargate/

 

 

 

EKS를 배포한다고 가정하면 어떤 인스턴스를 노드에 올릴지, 오토스케일링은 어떻게 할지 등을 고민해야 하는데 Fargate를 사용하면 Fargate profile(subnet, namespace, label conditions) 을 지정만 해 주면 되어 편리합니다.

https://www.eksworkshop.com/docs/fundamentals/fargate/

 

 

 

스팟 인스턴스

저는 주변의 비개발자에게 AWS를 공유오피스 사업자 내지는 임대업자(ㅋㅋㅋㅋ) 라고 소개합니다. 아무래도 AWS의 장점은 내가 원하는 스펙의 서버를 원하는 기간 동안 사용할 수 있다는 것인데요, 건물주에 비유해보면 AWS는 그만큼의 공실 부담을 지게 됩니다. 오피스와 달리 인스턴스는 임대와 회수가 매우 간편하며 즉각적이라는 점을 이용해 빈 인스턴스를 미리 예약해둔 유저에게 싼값에 빌려주는 대신 정가에 쓰겠다는 사용자가 요청하면 종료하는 것이 spot instance 입니다. 이때 종료알림 2분 후 인스턴스가 종료되므로 이 시간 안에 필요 데이터를 백업해야 합니다.

 

 

서울 리전에서 가장 비싼 인스턴스인 p4d.24xlarge를 예시로 들어보겠습니다. A100(VRAM 40GB) GPU 무려 8장이 붙어있는 인스턴스입니다. 그만큼 가격도 대단해서 온디맨드로 요청하면 시간당 $45가 부과됩니다💸

 

 

 

그러나 스팟인스턴스로 요청하면, 요금이 시간당 $7.9로 확 낮아지게 됩니다(대신 내가 원할 때 사용할 수 있다는 보장은 없습니다)

 

 

 

그러나 실제 스팟인스턴스 요청 화면에서는 사용자가 스팟인스턴스로 비딩(bidding) 할 가격과 기간을 지정할 수 있습니다. 적당한 요금을 비딩해서 스스로의 우선순위를 조정할 수 있겠습니다.

 

 

 

 

실습

우선 EKS를 terraform 으로 fargate에 배포 해 보겠습니다

사전 준비로 aws cli, admin권한을 가진 IAM, terraform, kubectl, helm 이 필요합니다.

 

 

 

그리고 github에서 실습코드를 클론해 옵니다.

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

 

 

 

 

이 테라폼 코드는 us-west-1 기준으로 작성되어 있기 때문에 서울리전에서 사용하기 위해서는 main.tf 의 local 블록을 수정해야 합니다.

가시다님께서 해당 부분을 수정한 테라폼 파일을 공유해주셔서 사용했습니다. 아래 토글에 있으니 필요하신 분은 참고하세요!

더보기
provider "aws" {
  region = local.region
}

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

provider "kubernetes" {
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)

  exec {
    api_version = "client.authentication.k8s.io/v1beta1" 
    command     = "aws"
    # This requires the awscli to be installed locally where Terraform is executed
    args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
  }
}

provider "helm" {
  kubernetes {
    host                   = module.eks.cluster_endpoint
    cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)

    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "aws"
      # This requires the awscli to be installed locally where Terraform is executed
      args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
    }
  }
}

data "aws_ecrpublic_authorization_token" "token" {
  provider = aws.virginia
}

data "aws_availability_zones" "available" {}

locals {
  name   = "t101-${basename(path.cwd)}"
  region = "ap-northeast-2"

  vpc_cidr = "10.10.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)

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

################################################################################
# Cluster
################################################################################

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.11"

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

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

  # Fargate profiles use the cluster primary security group so these are not utilized
  create_cluster_security_group = false
  create_node_security_group    = false

  enable_cluster_creator_admin_permissions = true

  fargate_profiles = {
    karpenter = {
      selectors = [
        { namespace = "karpenter" }
      ]
    }
    kube_system = {
      name = "kube-system"
      selectors = [
        { namespace = "kube-system" }
      ]
    }
  }

  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.name
  })
}

################################################################################
# EKS Blueprints Addons
################################################################################

module "eks_blueprints_addons" {
  source  = "aws-ia/eks-blueprints-addons/aws"
  version = "~> 1.16"

  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

  # We want to wait for the Fargate profiles to be deployed first
  create_delay_dependencies = [for prof in module.eks.fargate_profiles : prof.fargate_profile_arn]

  eks_addons = {
    coredns = {
      configuration_values = jsonencode({
        computeType = "Fargate"
        # Ensure that the we fully utilize the minimum amount of resources that are supplied by
        # Fargate https://docs.aws.amazon.com/eks/latest/userguide/fargate-pod-configuration.html
        # Fargate adds 256 MB to each pod's memory reservation for the required Kubernetes
        # components (kubelet, kube-proxy, and containerd). Fargate rounds up to the following
        # compute configuration that most closely matches the sum of vCPU and memory requests in
        # order to ensure pods always have the resources that they need to run.
        resources = {
          limits = {
            cpu = "0.25"
            # We are targeting the smallest Task size of 512Mb, so we subtract 256Mb from the
            # request/limit to ensure we can fit within that task
            memory = "256M"
          }
          requests = {
            cpu = "0.25"
            # We are targeting the smallest Task size of 512Mb, so we subtract 256Mb from the
            # request/limit to ensure we can fit within that task
            memory = "256M"
          }
        }
      })
    }
    vpc-cni    = {}
    kube-proxy = {}
  }

  enable_karpenter = true

  karpenter = {
    repository_username = data.aws_ecrpublic_authorization_token.token.user_name
    repository_password = data.aws_ecrpublic_authorization_token.token.password
  }

  karpenter_node = {
    # Use static name so that it matches what is defined in `karpenter.yaml` example manifest
    iam_role_use_name_prefix = false
  }

  tags = local.tags
}

resource "aws_eks_access_entry" "karpenter_node_access_entry" {
  cluster_name      = module.eks.cluster_name
  principal_arn     = module.eks_blueprints_addons.karpenter.node_iam_role_arn
  kubernetes_groups = []
  type              = "EC2_LINUX"
}

################################################################################
# Supporting Resources
################################################################################

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = local.name
  cidr = local.vpc_cidr

  azs             = local.azs
  private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)]
  public_subnets  = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)]

  enable_nat_gateway = true
  single_nat_gateway = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = 1
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = 1
    # Tags subnets for Karpenter auto-discovery
    "karpenter.sh/discovery" = local.name
  }

  tags = local.tags
}

 

 


전체를 배포하는 데 대략 15~20분 정도 소요됩니다 (vpc 배포 3분, eks 배포 13분)

terraform apply -target="module.vpc" -auto-approve
terraform apply -target="module.eks" -auto-approve

 

 

 

추가로 addon 을 설치합니다

terraform apply -auto-approve

 

 

 

저는 tag 없이 apply 해서 모두 한꺼번에 설치했습니다.

 

 

이번에는 AWS GUI console 에서 배포된 인스턴스를 확인해보겠습니다.

기존 EKS 스터디에서는 EC2 인스턴스를 노드로 사용했었는데 이번에는 fargate 로 배포했기 때문에 배포된 EC2는 없는 것을 볼 수 있습니다

 

 

 

대신 EKS 메뉴에 들어가면 인스턴스 유형이 'Fargate' 로 지정되어 4개의 노드가 배포된 것을 볼 수 있습니다

 

 

 

karpenter 실습

현재 클러스터에서는 karpenter를 이용해서 노드를 관리하고 있는데, 이 karpenter 를 이용해서 리소스가 추가 배포되었을 때의 액션을 확인해 보겠습니다. 현재 작업중인 경로에는 karpenter.yaml 이 있지만 우리의 실습에 맞게 클러스터 이름을 바꿔놓은 만큼 해당부분을 변령해줘야 합니다.

 

 

아래와 같이 karpenter.yaml 파일을 수정해줍니다.

apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2
  role: karpenter-t101-karpenter
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: t101-karpenter
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: t101-karpenter
  tags:
    karpenter.sh/discovery: t101-karpenter
---
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      nodeClassRef:
        name: default
      requirements:
        - key: "karpenter.k8s.aws/instance-category"
          operator: In
          values: ["c", "m", "r"]
        - key: "karpenter.k8s.aws/instance-cpu"
          operator: In
          values: ["4", "8", "16", "32"]
        - key: "karpenter.k8s.aws/instance-hypervisor"
          operator: In
          values: ["nitro"]
        - key: "karpenter.k8s.aws/instance-generation"
          operator: Gt
          values: ["2"]
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 30s

 

 

그리고 example.yaml 을 잠시 살펴보겠습니다. inflate 라는 deployment 는 처음에는 0개의 Replicaset 으로 지정되지만 replicaset이 그렇듯 갯수를 scale up/down 할 수 있습니다. 각 pod는 cpu를 1코어씩 점유하게 되므로 노드의 오토스케일링을 강제하게 됩니다

piVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
          resources:
            requests:
              cpu: 1

 

 

 

+DEBUG 이전/이후의 노드 개수를 확인해보기 위해 kubectl 명령어를 쳤는데 클러스터에 접근할 수 없다는 에러메시지가 발생했습니다. 분명히 클러스터를 배포했는데 다른 kubectl 명령어도 먹히지 않아 확인해보니 EKS가 아닌 로컬 클러스터에서 kubectl 명령어를 실행하려고 하니 발생하는 문제였습니다

따라서 cli에 아래 명령을 입력해주어 t101-karpenter 로 kubectl 명령어가 들어가게 설정했습니다.

aws eks --region ap-northeast-2 update-kubeconfig -
-name t101-karpenter

 

다른 스터디 멤버분의 블로그를 참고해 해결할 수 있었습니다. 감사드립니다!

 

 

이제 EKS cluster로 접근해서 fargate node 가 4개 배포되었음을 cli로도 확인할 수있습니다

클러스터에 karpenter와 example Deployment 를 배포해 줍니다

kubectl apply -f karpenter.yaml
kubectl apply -f example.yaml

karpenter 가 관리하는 nodepool과 ec2nodeclass 가 잘 배포되었음을 볼 수 있습니다.

nodepool은 실제 프로비저닝 동작을 제어하며, ec2nodeclass 를 참조하여 생성할 노드를 결정합니다

ec2nodeclass 는 노드의 기본 구성과 인스턴스의 세부사항을 지정합니다

example deployment 도 잘 배포되었습니다. 그렇지만 replicaset 의 갯수가 default=0으로 지정되어 있으므로 정상적으로 배포된 상태가 맞습니다. 부끄럽지만 yaml을 확인하지 않고 어 왜 배포가 안되지?! 하고서야 확인했습니다.

그렇다면 replica의 갯수를 늘리면서 동작의 변화를 보겠습니다. 아까 스펙에서 컨테이너 1개당 1core를 필요로 한다고 했기 때문에 3코어를 추가로 필요로 하게 됩니다.

kubectl scale deployment inflate --replicas=3

 

3개가 성공적으로 배포되었습니다

다시 node를 조회해 보면 스펙 변경에 대응하기 위해 1개가 추가된 것을 볼 수 있습니다.

 

 

리소스 삭제

helm uninstall kube-ops-view -n kube-system
terraform destroy -target="module.eks_blueprints_addons" -auto-approve
terraform destroy -target="module.eks" -auto-approve
terraform destroy -auto-approve

# vpc 삭제는 꼭 더블체크
aws ec2 describe-vpcs --filter 'Name=isDefault,Values=false' --output yaml

rm -rf ~/.kube/config

 

 

 

이렇게 편리하게 스펙변경에 대응할 수 있다는 점이 Fargate로 배포한 EKS 를 Terraform 으로 관리할 때 장점이라고 생각합니다. 정말 AWS에는 다양한 서비스가 있다는 것을 깨달았고 보다 더 자유자재로 쓰기 위해 정!진! 하겠습니다 🙃