2W - ALB Controller, External DNS

개요

실습 시기

스터디는 2주차에 이뤄졌으나, 몇 가지 이슈로 인해 내가 직접적으로 실습하고 정리하는 타이밍은 4주차가 됐다.
주차 별 내용 분리가 확실한 편이기 때문에, 한 주차에서 다룬 내용은 확실하게 해당 주차로 정리하도록 한다.

이번에는 EKS에서 활용할 수 있는 각종 네트워크 리소스를 알아본다.
구체적으로는 다음의 두 가지를 알아본다.

이를 통해 간단한 네트워크 지식, 운영 방법을 알게 될 것이다.

사전 지식

AWS Load Balancer Controller


ALB Controller는 ALB, NLB 등의 AWS의 로드밸런서를 클러스터의 서비스와 매핑시켜주는 컨트롤러다.
서비스, 인그레스 오브젝트를 만들 때 어노테이션으로 관련 설정을 넣어주면 이를 반영하여 AWS에 관련 리소스가 만들어지게 된다.
대략적인 동작 원리는 이렇다.

AWS api를 쓰기 때문에 해당 컨트롤러는 반드시 관련 리소스를 제어할 수 있는 적절한 권한을 가지고 있어야만 한다.

타겟 모드

AWS의 로드밸런서는 두 가지 유형의 타겟을 둘 수 있는데, ALB 컨트롤러 역시 이러한 기능을 사용할 수 있다.

Instance mode

흔한 로드밸런서의 동작과 같다.
로드밸런서라는 서비스는 사실 외부의 로드밸런서의 ip로 들어오는 요청에 대해 노드포트 규칙에 대응하게 하는 것밖에 없다.
클러스터로 들어가는 트래픽은 노드 포트를 통해 이뤄지며, 이쪽으로 트래픽을 흘려보내는 것을 외부 로드밸런서에 위임하는 것이 바로 로드밸런서 서비스이다.
instance 모드는 그냥 이 방식에 충실한 모드이다.

트래픽 정책에 대한 설정이 없다면 노드로 들어온 트래픽은 내부 iptables 규칙에 따라 분산된다.
그래서 기껏 로드밸런서가 트래픽을 분산해주는데 클러스터 내부로 와서 또 분산되는 꼴이 된다.
기껏 트래픽을 잘 분산시켰다고 생각했는데 내부적으로 또 분산을 해서 괜히 불필요한 트래픽이 발생할 가능성이 높다.

IP mode

이 방식은 로드밸런서가 파드의 IP에 직빵으로 트래픽을 분산해준다.

딱 봐도 한 단계의 hop이 줄고 트래픽 정리도 훨씬 간결해진다. ![image.png](https://841lgfvhej.execute-api.ap-northeast-2.amazonaws.com/default/image?url=https://gist.githubusercontent.com/Zerotay/5ba3bad7f8658466dac70beb7d3cd48f/raw/image.png) 이게 가능하려면 파드의 IP를 로드밸런서가 알 수 있어야 하고, 또 노드가 파드의 IP로 바로 접근할 수 있도록 지원을 해줘야 한다. 클러스터 외부에서 파드의 IP로 바로 접근할 수 있어야 한다는 말은? 파드가 클러스터 노드들의 IP 대역을 사용해야 한다는 것, 즉 이전에 봤던 VPC CNI를 사용해야만 사용가능한 방식이다. ### 사용 조건 ALB 컨트롤러를 활용하기 위해서 사전적으로 세팅해야 하는 요소는 다음과 같다. - 파드가 api에 통신을 할 수 있도록 하는 적절한 IAM 설정 - VPC CNI를 쓰고 있어야함 - internal, internet-facing을 구분 짓기 위한 서브넷 태그 설정 - internet-facing으로 인식되는 서브넷은 `kubernetes.io/role/elb` - internal로 인식되는 서브넷은 `kubernetes.io/role/internal-elb` - 이렇게 서브넷에 태그를 붙여두어야 로드밸런서가 만들어질 때 어떤 서브넷으로 연결될지를 지정할 수 있다. ## Readiness Gates

흔히 파드의 준비 상태를 판단하기 위해서 readinessProbe를 사용한다.

kind: Pod
apiVersion: v1
metadata:
  name: pod
spec:
  containers:
  - name: aews-websrv
	image: k8s.gcr.io/echoserver:1.5
	readinessProbe:
	  httpGet:
		path: /
		port: 8080
	  initialDelaySeconds: 3
	  periodSeconds: 3

이것은 kubelet이 컨테이너에 대해서 실행해주는 헬스체크로, 이를 통해 파드는 준비 상태를 나타낸다.
만약 이 헬스체크에 실패할 경우 해당 파드로 트래픽을 보내던 서비스는 바로 엔드포인트를 지운다.
이를 통해 실제 트래픽의 안정성을 도모할 수 있게 되는 것이다.
가령 어떤 한 파드가 큰 규모의 로직을 처리 중이라 다른 트래픽을 받을 수 없는 상태라면, 이럴 때 헬스체크가 실패하도록 로직을 구성해 해당 파드에 트래픽을 받지 않도록 하는 전략이 유효할 것이다.

그러나 로드밸런서를 사용하는 데에 있어서는 문제가 발생할 수 있다.
가령 파드는 이미 준비됐는데 아직 외부의 로드밸런서는 한창 프로비저닝이 되고 있다던가, 파드가 종료되었음에도 외부 로드밸런서는 아직 해당 파드가 건강하다고 생각하고 트래픽을 보내던가..
전자야 서비스에 있어서 큰 문제가 발생하지 않을 수 있으나, 후자는 실제 서비스의 장애를 일으키게 된다.
즉, 외부 로드밸런서와 내부의 파드의 준비 상태에 대한 명확한 동기화가 제대로 이뤄지게 하기 위한 설정이 필요하다는 말이다.
이때 사용하는 것이 바로 readniess Gates이다.
readiness Gate를 활용하면 로드 밸런서의 헬스체크에 따라 파드의 실질 준비 상태가 정해지기 때문에 훨씬 안전하게 트래픽 대응이 가능하다.

뭐.. 사실 후자의 케이스는 홀로 관리하는 사람의 케이스라면 거의 일어나지 않을 이슈라고 생각한다.
하지만 분업 환경에서는 일어날 가능성은 있는 실수라는 생각이 든다.
이럴 때에도 readiness gate는 분명 도움이 되어줄 것이다.

spec:
  readinessGates:
    - conditionType: "www.example.com/feature-1"

이런 식으로 파드 스펙에 readinessGates라는 필드를 작성하면 파드 상태에 해당 타입이 추가되고, 관련한 타입에 책임이 있는 컨트롤러가 외부 로드밸런서의 헬스체크 상태를 통해 준비 상태에 대한 조작을 진행해준다.
이 파드가 클러스터 외부 환경에서까지 구동될 준비가 되었다를 나타내는 지표라고 보면 될 것 같다.

External DNS

|600
쿠버네티스 외부에서 원하는 도메인을 붙일 수 있도록 도와주는 툴.
클러스터 내부에서는 CoreDNS를 통해 내부에서 사용하는 DNS가 만들어진다.
이것은 내부에서만 사용할 수 있는 것이고, 인터넷 상에서 고유한 것도 아니다.
그래서 인터넷 상의 고유한 도메인을 가지고 있을 때, 이 도메인을 클러스터의 네트워크 리소스에 연결하고자 할 때는 이 툴을 사용하면 된다.
image.png
다양한 DNS 프로바이더에 대해 설정하는 방법이 나와있다.
클러스터 외부와 통신을 하는 컨트롤러가 설치되기 때문에, 이 친구가 관련한 작업을 할 수 있도록 권한 등의 추가 세팅을 해야 한다.

kubectl annotate service tetris "external-dns.alpha.kubernetes.io/hostname=tetris.$DOMAIN"

사용법은 그냥 어노테이션을 붙이는 방식이다.
실습에서 본격적으로 확인하겠다.

실습 진행

ALB Controller 배포

헬름 및 cli를 통한 설정

이 방법은 처음 내가 시도한 방법으로, 조금 삽질을 거친 과정을 담는다.
이후에는 테라폼을 통해 설정을 진행했다.

helm repo add eks https://aws.github.io/eks-charts 
helm repo update 
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME

설치 자체는 참 간단하다.

k describe deployments.apps -n kube-system aws-load-balancer-controller

어떤 식으로 배포됐는지도 확인해본다.
image.png
컨트롤러 컨테이너에는 인자로 현재 클러스터 네임과, 인그레스 클래스가 될 이름이 들어가있다.
서비스 어카운트는 aws-load-balancer-controller이고, 이 친구가 aws의 로드밸런서를 만들 수 있는 권한이 있어야만 제대로 동작한다.
그 권한은 IRSA로 세팅되는데, 제대로 설정이 된다면 서비스 어카운트의 어노테이션에 eks.amazonaws.com/role-arn가 붙어있어야 한다.
자세한 내용은 보안 주차에서 다룬다.
image.png
이렇게 인그레스 클래스를 확인할 수 있다.

kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding 
kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role

해당 어카운트가 어떤 롤을 가지고 있는지도 확인해보자.
image.png
네트워크와 관련된 다양한 권한을 가지고 있는 것이 확인된다.
이렇게 클러스터 내부의 리소스를 컨트롤러가 접근하여 각종 작업을 해줄 수 있을 것이다.

그럼 이제 클러스터 외부에 컨트롤러가 조작을 할 수 있도록 IRSA를 세팅해보자.[1]
안타깝게도.. 이 친구들은 폴리시 자체는 문서에서 다운 받을 수 있게 해뒀지만 아예 공식적으로 제공해주지는 않는다.

curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.11.0/docs/install/iam_policy.json

그래서 이걸 받아서 직접 정책을 만들고, 이걸 롤에 붙여주는 작업을 직접 해줘야 한다.[2]

aws iam create-policy \
    --policy-name AWSLoadBalancerControllerIAMPolicy \
    --policy-document file://iam_policy.json

정책을 미리 만들어서 올려둔다.

eksctl create iamserviceaccount \
  --cluster=my-cluster \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --role-name AmazonEKSLoadBalancerControllerRole \
  --attach-policy-arn=arn:aws:iam::111122223333:policy/AWSLoadBalancerControllerIAMPolicy \
  --approve

그 다음에 eksctl을 사용할 경우, 이렇게 서비스어카운트에 대해 어떤 롤을 붙여줄지를 지정한다.
이때 해당 서비스어카운트에 연결될 롤과 정책을 한번에 지정해버린다.

image.png
직접 서비스 어카운트에 해당 값을 넣어본다.
image.png
그러나 vpc id에 접근할 수 없다는 에러가 떴고, 이를 직접적으로 넣어주는 방법을 시도했다.
인스턴스의 IMDSv2를 쓰는 것도 가능하지만, 설정이 되려 복잡해진다고 판단했다.

helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME --set vpcId=$VPCID --set region=ap-northeast-2

컨트롤러를 담당하는 디플로이먼트에 직빵으로 인자를 수정하는 것도 가능하겠으나, 환경 유지 편의성을 위해 이렇게 세팅했다.
image.png
그러나 단순히 이렇게하면 문제가 생기는데, 헬름 차트에서 sa를 만들도록 할 경우 원하는 어노테이션이 제대로 부착되지 않기 때문이다.
그래서 k edit sa를 해서 직접적으로 붙이면 최소한 이 에러는 해결된다.
image.png
그런데 또! 막상 컨트롤러가 잘 돌아간다고 해도 주의해야 한다.
sa가 제대로 반영된 채로 컨트롤러가 동작하지 않으면 이렇게 서비스 쪽에서 에러가 발생한다.
sa가 파드 생성 시점에 한번 볼륨으로 만들어져 마운팅되기에, 알아서 변경이 파드에 반영되지 않는다.
image.png
제대로 변경해주면 이렇게 성공한다!
image.png
그럼 이제 헬름 설정을 할 때 조금 더 제대로 세팅을 할 수 있게 해보자.
롤을 어노로 붙이려면 이 세팅을 하면 될 것이다.
아니면 보통 대중적으로 하는 방식은, 미리 SA를 직접 커스텀해서 만든 후에 여기에서 create=false를 하는 것이다.
내 생각에는 그냥 여기에서 해당 롤을 어노테이션으로 붙이게 하는 것도 괜찮을 것 같다.

테라폼을 통한 설정

여기에 매번 붙이는 방식은 귀찮으니, 이제 테라폼으로 헬름까지 세팅한다.

locals {
  lbc_name_in_aws = "AWSLoadBalancerController"
  lbc_name_in_cluster = "aws-load-balancer-controller"
  lbc_namespace = "kube-system"
}

provider "helm" {
  kubernetes {
    config_path = "./kubeconfig"
  }
}

resource "helm_release" "lbc" {
  name       = local.lbc_name_in_cluster
  repository = "https://aws.github.io/eks-charts"
  chart      = local.lbc_name_in_cluster

  namespace = local.lbc_namespace

  set {
    name = "clusterName"
    value = module.eks.cluster_name
  }
  set {
    name = "region"
    value = data.aws_region.current.name
  }
  set {
    name = "vpcId"
    value = module.eks_vpc.vpc_id
  }
  set {
    name  = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
    value = aws_iam_role.lbc.arn
  }
}

data "http" "lbc" {
  url = "https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.11.0/docs/install/iam_policy.json"
}
resource "aws_iam_policy" "lbc" {
  name        = "${local.lbc_name_in_aws}IAMPolicy"
  description = "IAM Policy for AWS Load Balancer Controller"
  policy      = data.http.lbc.response_body
}

data "aws_iam_policy_document" "lbc" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    principals {
      type        = "Federated"
      identifiers = [module.eks.oidc_provider_arn]
    }

    condition {
      test     = "StringEquals"
      variable = "${replace(module.eks.cluster_oidc_issuer_url, "https://", "")}:sub"

      values = [
        "system:serviceaccount:${local.lbc_namespace}:${local.lbc_name_in_cluster}",
      ]
    }
    effect = "Allow"
  }
}
resource "aws_iam_role" "lbc" {
  name               = "${local.lbc_name_in_aws}IAM"
  assume_role_policy = data.aws_iam_policy_document.lbc.json
}

resource "aws_iam_role_policy_attachment" "lbc" {
  role       = aws_iam_role.lbc.name
  policy_arn = aws_iam_policy.lbc.arn
}

간단하게 코드를 설명하자면, 일단 문서에 나온 최소 기준으로 json파일을 가져와서 그대로 정책을 만든다.
이후에 롤까지 만드는데, 이때 assume을 할 수 있는 롤을 명시할 때는 해당 롤을 받을 수 있는 주체는 eks 클러스터의 OIDC로부터 인증을 받은 주체만 가능하도록 한다.
이를 체크하는 것이 sts:AssumeRoleWithWebIdentity이고, identifiers에는 oidc 프로바이더를 명시해주면 된다.
확실치 않은 부분으로, 프로바이더의 arn을 넣어주는데 외부 oidc를 사용할 수 없는 것인지는 잘 모르겠다.
아무튼 이를 통해 프로바이더에게 받은 토큰에 sub 클레임을 확인하여 로드밸런서 컨트롤러의 서비스 어카운트가 가질 값을 가지는지 확인한다.
이를 통과한 주체라면 로밸컨이 가져야 하는 정책을 활용할 수 있게 된다.

또한 헬름 리소스에서는 알아서 서비스 어카운트가 만들어지고, 이 어카운트의 어노테이션에 연결될 롤의 arn을 넣음으로써 IRSA가 성립하게 된다.

서비스.로드밸런서 테스트

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: aews-websrv
        image: k8s.gcr.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-ip-type
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    app: deploy-websrv

로드 밸런서로서 쓸 때는 위와 같이 어노테이션을 달아주면 된다.
간단하게 말하자면, 퍼블릭 서브넷에 붙으며 ip 모드로 동작할 수 있도록 만든 것이다.
(아래에서 볼 인그레스와 어노테이션 양식이 다르니 차이를 명심하자.)
image.png
서비스에 loadBalancerClass를 지정하는 것이 보이는데, 이 값은 어차피 넣지 않아도 Admission Webhook을 통해 알아서 주입된다.
image.png
콘솔이나 k get svc로 나온 dns 이름을 확인하고 브라우저로 접속한 결과이다.
기본적으로 nlb는 클라이언트 ip를 보존하지 않으므로 request ip가 로드밸런서의 주소가 나온다.
image.png
또한 어노테이션으로 일일히 설정해준 덕분에 실제 타겟들의 헬스 상태를 콘솔에서도 체크할 수 있다.
이게 가능한 이유는 IP 모드를 썼기 때문으로, 로드밸런서가 실제 파드의 ip를 알고 헬스체크를 수행해주는 것이다.

NLB=$(k get svc svc-nlb-ip-type | awk 'NR==2 {print $4}')
for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr

로드밸런싱이 잘 되는지도 테스트해본다.
image.png
나온 출력을 정렬하면 고유값을 꺼내는 게 가능해져서 이렇게 편하게 결과를 확인할 수 있다.

k rollout deployment deploy-echo

image.png
ip 모드는 실제 파드의 상태를 체크하여 등록 해제와 새로운 등록을 알아서 해준다.

k get targetgroupbindings.elbv2.k8s.aws -o yaml

image.png
alb 컨트롤러는 aws의 로드밸런서가 가진 타겟 그룹을 클러스터에도 미러링하여 생성해준다.
그래서 생성된 타겟그룹에 대한 정보를 오브젝트로 이렇게 확인할 수 있다.
간단하게 보안 그룹, vpc id까지도 확인 가능한 것이 보인다.
이걸 활용해 아예 aws에 이미 존재하는 타겟그룹을 클러스터에 만들어서 관리하는 것도 가능하다.

image.png
(그림은 다른 실습에서 따왔다)
추가적으로, 만약 ip모드가 아니라 인스턴스 모드로 로드밸런서를 만들면 타겟이 인스턴스로 설정된다.
타겟이 3개인 이유는 현재 실행 중인 노드가 3개이기 때문에 그렇다.
이때는 어디로는 트래픽이 일단 송신되고, 각 인스턴스의 iptables 룰에 의해 알아서 또 트래픽이 라우팅될 것이다.

타겟 그룹 속성 세팅

service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: >
  deregistration_delay.timeout_seconds=60,
  preserve_client_ip.enabled=true

이번에는 어노테이션으로 타겟 그룹에 대한 속성을 넣어보자.
대상 그룹에 대해 설정할 수 있는 것들은 전부 있다고 보면 된다.[3]
여러 개를 넣을 때는 이렇게 쉼표를 통해서 넣어주면 되는데, 그냥 넣으면 길어지니 >을 통해 folding 형태로 넣어주도록 한다.
이런 변경사항은 alb 컨트롤러가 계속 감시하며 어노테이션의 변경을 인지하고 동적으로 반영해준다.
설정한 값은 문제 생긴 대상을 빠르게 지워지도록 하는 60초 설정, 그리고 실제 client의 ip가 보존되도록 하는 기능이다.
image.png
이번에는 우리 집 ip가 나오는 것이 확인된다.
image.png
콘솔에서는 관련한 설정들이 제대로 들어간 것을 확인할 수 있다.

레디네스 게이트 활용

k label namespaces default elbv2.k8s.aws/pod-readiness-gate-inject=enabled

ALB 컨트롤러에서 레디네스 게이트는 사용하고자 하는 네임스페이스에 라벨을 달아 설정해주는 방식으로 운영된다.
이 네임스페이스에서 생성되는 서비스에 대해서는 컨트롤러가 알아서 대상이 되는 파드에 레디네스 게이트를 주입시킨다.
image.png
해당 네임스페이스에 배치되는 파드들에 readinessGates가 주입되는 것이 보인다.
image.png
파드에 readiness gate가 설정되고, 이 값은 실제 로드 밸런서에서 헬스체크가 완료되었을 때 비로소 1이 된다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo-2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: aews-websrv
        image: nginx
        readinessProbe:
          httpGet:
            path: /
            port: 80

만들어진 서비스에 엔드포인트로 잡히지만, 8080포트로 소켓을 열어두지 않는 임의의 파드를 하나 더 만들었다.
image.png
보다시피 이 파드는 ready 상태이나, readiness gate는 계속 실패 상태이다.
image.png
마찬가지로 콘솔에서도 헬스체크는 실패 상태이다.
명확하게 파드의 상태를 로드밸런서가 감지하고 있는 것을 알 수 있다.
참고로 인스턴스 모드에서는 이를 쓸 수 없다.

	k get ep svc-nlb-ip-type -o yaml

image.png
이렇게 레디네스를 설정하면 실제로 서비스에서도 엔드포인트를 준비되지 않았다고 설정해준다.
그래서 외부 로드밸런서가 트래픽을 보내지 않음은 물론, 클러스터 내부에서도 해당 파드로 트래픽을 보내지 않게 된다.

번외 - 레디네스 게이트가 설정되지 않은 상태에서 잘못 설정해보기

image.png
번외로, 레디네스 게이트가 설정되지 않은 네임스페이스에서는 보다시피 파드의 ready 상태만 따져서 엔드포인트가 추가되는 것을 볼 수 있다.
레디네스 게이트.. 써야겠지?

k run -ti -n test debug --image nicolaka/netshoot -- zsh

---
for i in {1..100}; do 
  if curl -s svc-nlb-ip-type.test | grep -q "Hostname"; then 
    echo "success"; 
  else 
    echo "fail"; 
  fi; 
done | sort | uniq -c | sort -nr

실제로 해당 네임스페이스에서 서비스로 계속 호출을 날려보면..
image.png
서비스가 간혹 실패하는 것이 보인다.

인그레스 테스트

k get ingressclass alb -oyaml | yh

로드밸런서 클래스와 다르게, 인그레스는 확실하게 클래스 오브젝트가 존재한다.
image.png
spec 하위 필드로 parameterts 필드를 넣음으로써 여러 설정이 가능한데, ALB 컨트롤러에서는 이를 위해 IngressClassParams라는 CRD를 따로 제공한다.

  parameters:
    apiGroup: k8s.example.com
    kind: IngressParameters
    name: external-lb

세팅할 때는 이런 식으로 넣어주면 되는데, 현재 기본 클래스에는 없는 것이 보인다.
아래 추가적으로 이에 대한 실습을 진행할 것이다.
image.png
그런데 재밌는 게, 사용도 하지 않을 거면서 기본으로 ingressclass params도 만들어 둔 게 보인다.

이제 기본적인 실습을 해보자.

apiVersion: v1
kind: Namespace
metadata:
  name: game-2048
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: game-2048
  name: deployment-2048
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: app-2048
  replicas: 2
  template:
    metadata:
      labels:
        app.kubernetes.io/name: app-2048
    spec:
      containers:
      - image: public.ecr.aws/l6m2t8p7/docker-2048:latest
        imagePullPolicy: Always
        name: app-2048
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  namespace: game-2048
  name: service-2048
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: NodePort
  selector:
    app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: game-2048
  name: ingress-2048
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: service-2048
              port:
                number: 80

2048 게임을 배포해보자!
다시 말하지만 인그레스와 로드밸런서는 사용하는 어노테이션 이름이 다르다.
image.png
성공적으로 인그레스가 만들어진 것이 보인다.
image.png
브라우저로도 정상적으로 들어갈 수 있다.
image.png
보다시피 인그레스에 대해서는 alb가 만들어지는 것이 보인다.

인그레스 그룹 세팅

이번에는 여러 인그레스를 하나의 ALB에 묶어본다.
image.png
일전에 내가 미리 만들어둔 이미지를 활용해본다.
이 이미지는 자신의 현 버전 정보를 출력하도록 세팅됐다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: zero4
  annotations:
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/group.name: aews
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /4
          pathType: Prefix
          backend:
            service:
              name: zero4
              port:
                number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: zero5
  annotations:
    alb.ingress.kubernetes.io/target-type: instance
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/group.name: aews
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
        - path: /5
          pathType: Prefix
          backend:
            service:
              name: zero5
              port:
                number: 80

어노테이션으로 그룹을 넣어주면, 두 인그레스로 보여도 실제 리소스가 만들어질 때는 하나의 alb만이 만들어진다.
image.png
그룹 이름을 통해 로드 밸런서가 만들어졌으며, 뒷단에는 인스턴스와 ip 모드를 가진 서비스를 두고 있다.
image.png
4, 5를 경로로 설정했는데 둘다 제대로 값이 나오는 것을 볼 수 있다.

IngressClassParams 세팅

각종 설정들을 매번 어노테이션해주는 것은 퍽 귀찮은 일이다.
어노테이션이란 것 자체가 애초에 만드는 시점에 유효성 검증을 하는 것도 어려워서 많은 혼란을 야기한다.
그래서 이걸 통합적으로 관리할 수 있도록 기본 인그레스 클래스에 조금의 설정을 넣자.
image.png
위에서 봤듯이 이미 인그레스 클래스지만 파라미터는 없다.
이 양식을 받아서 수정하고, 동시에 IngressClassParams 오브젝트를 만든다.

# Even if you apply this setting, ingress still needs to be set "name", "target-mode" FUCK
apiVersion: elbv2.k8s.aws/v1beta1
kind: IngressClassParams
metadata:
  name: default-icp
spec:
  scheme: internet-facing
  group:
    name: aews
  loadBalancerAttributes:
    - key: routing.http.xff_client_port.enabled
    value: "true"
  # certificateArn: ['arn:aws:acm:us-east-1:123456789:certificate/test-arn-1','arn:aws:acm:us-east-1:123456789:certificate/test-arn-2']
  tags:
  - key: org
    value: aews
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  annotations:
    meta.helm.sh/release-name: aws-load-balancer-controller
    meta.helm.sh/release-namespace: kube-system
  labels:
    app.kubernetes.io/instance: aws-load-balancer-controller
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: aws-load-balancer-controller
    app.kubernetes.io/version: v2.11.0
    helm.sh/chart: aws-load-balancer-controller-1.11.0
  name: alb
spec:
  controller: ingress.k8s.aws/alb
  parameters:
    apiGroup: elbv2.k8s.aws 
    kind: IngressClassParams 
    name: default-icp

나는 이런 식으로 만들어주었다.
일단 이 클래스를 이용하면 그룹이 무조건 통일된다.
또한 ALB 헤더에 XFF를 추가해 클라의 ip를 보존할 수 있도록 세팅해주었다.
근데 타겟 모드, 도메인 이름은 결국 사용하는 리소스에서 어노테이션을 달긴해야 한다..
최소한 서브 도메인 postfix 관련 세팅이라도 있으면 좀더 세팅이 간편해질텐데.
image.png
설정을 적용하니 기존의 인그레스클래스에 parameter가 생기는 것이 보인다.
이 상태로 위에서 사용한 인그레스에서 그룹 관련 주석만 제거하고 다시 실행해본다.

내가 만든 이미지 0.0.6 버전은 XFF의 헤더 값이 있으면 이를 읽고 클라이언트 ip로서 출력한다.
인그레스 그룹 양식 파일을 조금 수정해서 다시 진행해본다.
0.0.4 버전을 0.0.6으로 바꾸고, 어노테이션에 그룹을 없앴다.
image.png
6버전에서는 클라이언트 ip로 내 로컬 주소가 나오는 것이 확인된다.
image.png
그룹 세팅이 이미 들어가있기에 하나의 로드밸런서에 두 인그레스가 묶였다.

External DNS 활용

마지막으로 dns를 써본다.
이를 위해서는 먼저 route53에 나만의 도메인을 가지고 있어야만 한다.

테라폼 세팅

나는 역시 테라폼을 이용해 세팅을 진행한다.

locals {
  domain_name = "zerotay.com."
  ext_name_in_cluster = "external-dns"
  ext_namespace = "kube-system"
}

data "aws_route53_zone" "this" {
  name         = local.domain_name
  private_zone = false
}
module "external_dns_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "5.52.2"
  role_name = "external-dns"
  attach_external_dns_policy    = true
  external_dns_hosted_zone_arns = [data.aws_route53_zone.this.arn]
  force_detach_policies = true
  oidc_providers = {
    eks = {
      provider_arn = data.aws_iam_openid_connect_provider.this.arn
      namespace_service_accounts = ["kube-system:external-dns"]
    }
  }
}

resource "helm_release" "external_dns" {
  name       = local.ext_name_in_cluster
  repository = "https://kubernetes-sigs.github.io/external-dns"
  chart      = "external-dns"

  namespace = local.ext_namespace
  values = [
    <<-EOF
    env:
      - name: AWS_DEFAULT_REGION
        value: ${data.aws_region.current.name}
    provider:
      name: aws
    policy: upsert-only
    serviceAccount:
      annotations:
        "eks.amazonaws.com/role-arn": ${module.external_dns_irsa.iam_role_arn}
    EOF
  ]
}

세팅은 이렇게 해주었다.
일단 data로 현재 내가 가지고 있는 도메인의 각종 정보를 가져온다.
이후에 irsa를 설정을 해준 후에 헬름으로 배포!

참고로 여기에서 policy가 upsert-only인데, 이것은 클러스터에서 설정된 레코드가 추가, 변경되는 것만 가능하게 한다.
해당 리소스의 삭제는 레코드에 영향을 미치지 못하게 하는 건데, 실제 서비스 상황에서는 이런 설정이 서비스의 안정성을 유지하는데 도움을 줄 것이다.

테스트

image.png
성공적으로 컨트롤러가 설치됐다.

k get -n kube-system sa external-dns -o yaml | yh

image.png
IRSA 설정도 제대로 들어간 것이 확인된다.

그럼 간단한 워크로드를 배포해서 확인해보자.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tetris
  labels:
    app: tetris
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tetris
  template:
    metadata:
      labels:
        app: tetris
    spec:
      containers:
      - name: tetris
        image: bsord/tetris
---
apiVersion: v1
kind: Service
metadata:
  name: tetris
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
    #service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "80"
spec:
  selector:
    app: tetris
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb

기본 앱을 만들었다.
아직 external dns를 적용한 상태는 아니다.

DOMAIN=zerotay.com
kubectl annotate service tetris "external-dns.alpha.kubernetes.io/hostname=tetris.$DOMAIN"

어노테이션을 추가해본다.
image.png
적용하더라도 콘솔에서도, 클러스터에서도 해당 사실이 직접적으로 드러나진 않는다.

k -n kube-system logs external-dns...

image.png
하지만 설치된 컨트롤러의 로그를 보면 확실하게 동작을 한 것을 확인할 수 있다.
image.png
알아서 해당 도메인에 A레코드가 추가되어 질의도 가능해지는 것이 보인다.
image.png
실제로도 잘 들어가는 것이 확인된다!

결론

로드밸런서 IP 모드를 활용하며 명확하게 실제 파드의 헬스체크가 가능해지므로 이를 활용하여 서비스의 안정성을 높일 수 있다.
이를 쓰기 위해서는 vpc cni를 써야만 하므로, EKS에서는 가급적 VPC CNI를 쓰도록 하자.

또한 기본적인 로드밸런서를 이용하면 도메인 이름이 매우 길고 어렵게 나오게 되는데, external dns를 이용하면 기존에 소유하고 있는 도메인으로 서비스에 접속되도록 편하게 설정할 수 있다.

이전 글, 다음 글

다른 글 보기

이름 index noteType created
1W - EKS 설치 및 액세스 엔드포인트 변경 실습 1 published 2025-02-03
2W - 테라폼으로 환경 구성 및 VPC 연결 2 published 2025-02-11
2W - EKS VPC CNI 분석 3 published 2025-02-11
2W - ALB Controller, External DNS 4 published 2025-02-15
3W - kubestr과 EBS CSI 드라이버 5 published 2025-02-21
3W - EFS 드라이버, 인스턴스 스토어 활용 6 published 2025-02-22
4W - 번외 AL2023 노드 초기화 커스텀 7 published 2025-02-25
4W - EKS 모니터링과 관측 가능성 8 published 2025-02-28
4W - 프로메테우스 스택을 통한 EKS 모니터링 9 published 2025-02-28
5W - HPA, KEDA를 활용한 파드 오토스케일링 10 published 2025-03-07
5W - Karpenter를 활용한 클러스터 오토스케일링 11 published 2025-03-07
6W - PKI 구조, CSR 리소스를 통한 api 서버 조회 12 published 2025-03-15
6W - api 구조와 보안 1 - 인증 13 published 2025-03-15
6W - api 보안 2 - 인가, 어드미션 제어 14 published 2025-03-16
6W - EKS 파드에서 AWS 리소스 접근 제어 15 published 2025-03-16
6W - EKS api 서버 접근 보안 16 published 2025-03-16
7W - 쿠버네티스의 스케줄링, 커스텀 스케줄러 설정 17 published 2025-03-22
7W - EKS Fargate 18 published 2025-03-22
7W - EKS Automode 19 published 2025-03-22
8W - 아르고 워크플로우 20 published 2025-03-30
8W - 아르고 롤아웃 21 published 2025-03-30
8W - 아르고 CD 22 published 2025-03-30
8W - CICD 23 published 2025-03-30
9W - EKS 업그레이드 24 published 2025-04-02
10W - Vault를 활용한 CICD 보안 25 published 2025-04-16
11W - EKS에서 FSx, Inferentia 활용하기 26 published 2025-04-18
11주차 - EKS에서 FSx, Inferentia 활용하기 26 published 2025-05-11
12W - VPC Lattice 기반 gateway api 27 published 2025-04-27

관련 문서

이름 noteType created
AWS Load Balancer Controller knowledge 2025-02-12

참고


  1. https://malwareanalysis.tistory.com/724 ↩︎

  2. https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/lbc-helm.html ↩︎

  3. https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/network/load-balancer-target-groups.html#target-group-attributes ↩︎