본문 바로가기
프로젝트&&스터디/AWES 3기 (2025.01~)

AEWS 3기 - EKS 네트워킹 (2) EKS Loadbalancer Controller, Ingress, ExternalDNS

by james_janghun 2025. 2. 15.
이 글은 가시다님과 함께하는 AEWS 3기의 내용을 정리한 것입니다.

EKS Loadbalancer Controller

https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/

 

Welcome - AWS Load Balancer Controller

Welcome A Kubernetes controller for Elastic Load Balancers AWS Load Balancer Controller AWS Load Balancer Controller is a controller to help manage Elastic Load Balancers for a Kubernetes cluster. This project was formerly known as "AWS ALB Ingress Control

kubernetes-sigs.github.io

 

LoadBalancer Controller는 말그대로 로드밸런서를 컨트롤 해주는 플러그인 같은 것이다. 우리가 기본적으로 쿠버네티스에서 Loadbalancer 타입의 service를 생성하거나 ingress를 생성할 때 LBController가 AWS 내에서 LB 생성을 돕는다.

 

뿐만아니라 더 나아가서 targetgroupbinding을 통해서 타겟그룹도 자동으로 만들어 연결해주는 역할을 하는 등 아주 필수적인 존재라고 할 수 있다.

 

LoadBalancer Controller

 

로드밸런서 컨트롤러는 위와 같은 아키텍처를 갖는다. 각 단계는 다음과 같다.

1) LB컨트롤러가 API 서버로 부터 수신 이벤트를 감지하고, 요구사항을 충족하는(주로 필터) 수신 리소스를 찾으면 AWS에서 리소스를 생성한다.

 

2) 새로운 ingress 리소스에 대해 AWS ALB가 생성되고, 이 ALB가 인터넷 연결이나 내부(internal)로 설정된다. 이는 annotation으로 설정가능하다.

 

3) Ingress 리소스와 연결된 kubernetes service에 대해서 AWS에 Targetgroup을 생성한다.

 

4) 수신 리소스 annotation에 설정된 포트 정보에 따라 포트를 설정한다.

 

5) Ingress 리소스에 지정된 각 경로에 의해 routing rule을 결정한다. 

 

이 뿐 아니라 쿠버네티스에서 리소스가 삭제되면 LB 컨트롤러가 감지하여 AWS에서도 연관 리소스를 삭제한다.

 

 

helm을 통한 aws-load-balancer-controller 배포

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

 

 

자세한 설치가이드와 내용이 궁금하면 다음 공식문서를 확인하자.

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

 

Helm를 사용하여 AWS Load Balancer Controller 설치 - Amazon EKS

AWS Management Console에서 정책을 보는 경우 콘솔에 ELB 서비스에 대한 경고는 표시되지만 ELB v2 서비스에 대한 경고는 표시되지 않습니다. 이는 정책의 작업 중 일부가 ELB v2에는 있지만 ELB에는 없기

docs.aws.amazon.com

 

 

이제 서비스를 하나 만들어보자

cat << EOF > echo-service-nlb.yaml
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
EOF
kubectl apply -f echo-service-nlb.yaml

서비스가 로드밸런서 타입이기 때문에 로드밸런서가 실제로 생성되었고, 연결된 pod에 맞게 타겟그룹이 생성되었다.

 

타겟그룹을 한번 살펴보자.

여기서 등록 취소 지연(드레이닝 간격)이라는 것이 있는데 이 값은 일종의 graceful 설정과 비슷하게

300초 이내의 준비시간을 준다는 말이다. 이는 기본값이 300이다.

이런 모든 네트워크 등의 설정 값들은 대부분 annotation에서 관리되고 있다.

따라서 서비스 yaml에서 deregistration_delay.timeout_seconds=60을 추가해보자.

    service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: deregistration_delay.timeout_seconds=60

 

 

바로 즉시 적용되는 걸 확인할 수 있다.

 

AWS LB Contoller를 통한 A/B테스트

ALB의 기본동작은 다음과 같다.

- 대상그룹에 가중치 적용

- 고급 요청 라우팅

- annotation을 통한 통제

 

실제로 살펴보면서 확인해보자. 다음 예시를 통해서 실습해볼 예정이다.

git clone https://github.com/paulbouwer/hello-kubernetes.git
tree hello-kubernetes/

 

버저닝이 되어있는 예시 애플리케이션을 배포해보자.

# Install sample application version 1
helm install --create-namespace --namespace hello-kubernetes v1 \
  ./hello-kubernetes/deploy/helm/hello-kubernetes \
  --set message="You are reaching hello-kubernetes version 1" \
  --set ingress.configured=true \
  --set service.type="ClusterIP"

# Install sample application version 2
helm install --create-namespace --namespace hello-kubernetes v2 \
  ./hello-kubernetes/deploy/helm/hello-kubernetes \
  --set message="You are reaching hello-kubernetes version 2" \
  --set ingress.configured=true \
  --set service.type="ClusterIP"

# 확인
kubectl get-all -n hello-kubernetes
kubectl get pod,svc,ep -n hello-kubernetes
kubectl get pod -n hello-kubernetes --label-columns=app.kubernetes.io/instance,pod-template-hash

 

 

배포 후 ingress에 blue-green annotation을 설정한다.

v1(blue)에 가중치 100을 할당했다.

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: "hello-kubernetes"
  namespace: "hello-kubernetes"
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/actions.blue-green: |
      {
        "type":"forward",
        "forwardConfig":{
          "targetGroups":[
            {
              "serviceName":"hello-kubernetes-v1",
              "servicePort":"80",
              "weight":100
            },
            {
              "serviceName":"hello-kubernetes-v2",
              "servicePort":"80",
              "weight":0
            }
          ]
        }
      }
  labels:
    app: hello-kubernetes
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: blue-green
                port:
                  name: use-annotation
EOF

 

 

리스너 규칙에서도 잘 반영된것을 확인할 수 있습니다.

 

가중치를 변경하려면 (green으로 넘어가려면) annotation정보만 새로 반영해주면됩니다.

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: "hello-kubernetes"
  namespace: "hello-kubernetes"
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/actions.blue-green: |
      {
        "type":"forward",
        "forwardConfig":{
          "targetGroups":[
            {
              "serviceName":"hello-kubernetes-v1",
              "servicePort":"80",
              "weight":90
            },
            {
              "serviceName":"hello-kubernetes-v2",
              "servicePort":"80",
              "weight":10
            }
          ]
        }
      }
  labels:
    app: hello-kubernetes
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: blue-green
                port:
                  name: use-annotation
EOF

 

 

 

Ingress

클러스터 내부 서비스를 외부로 노출할때 HTTP/HTTPS 프로토콜을 쓰는 L7단의 Web Proxy 역할을 담당한다.

다음과 같이 2048게임을 Ingress로 배포해보자.

# 게임 파드와 Service, Ingress 배포
cat <<EOF | kubectl apply -f -
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
EOF

 

이제 리소스 생성을 마친후 내용을 확인해보자.

kubectl describe ingress -n game-2048 ingress-2048

 

deployment가 service로 연결되어있고, 해당 service가 ingress를 통해서 가져가게된다.

 

AWS에서도 해당 내용을 확인할 수 있다.

# ALB 생성 확인
aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-game2048`) == `true`]' | jq
ALB_ARN=$(aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-game2048`) == `true`].LoadBalancerArn' | jq -r '.[0]')
aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN
TARGET_GROUP_ARN=$(aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN | jq -r '.TargetGroups[0].TargetGroupArn')
aws elbv2 describe-target-health --target-group-arn $TARGET_GROUP_ARN | jq

 

ingress는 ALB를 사용하기 때문에 HTTP 헤더를 읽을 수 있다. 그래서 리스너설정에서 경로패턴도 들어가게된다.

 

주소로 접속해보자. 게임을 확인할 수 있다.

kubectl get ingress -n game-2048 ingress-2048 -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' | awk '{ print "Game URL = http://"$1 }'

 

 

 

External DNS

도메인 서비스를 연결하는 것으로 실제 우리 도메인을 연결해볼 수 있다. 기본적으로는 AWS Route53에서 직접 등록해도되지만 수동으로 관리해야한다. 이를 자동으로 진행하기 위해서 External DNS를 사용한다면 별도로 신경쓰지 않아도 자동으로 등록된다.

 

내가 실제로 사용하는 도메인이 있어서 이걸로 진행해보자.

 

도메인의 호스트존 정보를 일단 가지고 있는다.

MyDnzHostedZoneId=`aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text`
echo $MyDnzHostedZoneId

 

다음 명령어로 External DNS를 설치한다.

curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
cat externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

 

externaldns.yaml의 정보는 다음과 같다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
  labels:
    app.kubernetes.io/name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: external-dns
  labels:
    app.kubernetes.io/name: external-dns
rules:
  - apiGroups: [""]
    resources: ["services","endpoints","pods","nodes"]
    verbs: ["get","watch","list"]
  - apiGroups: ["extensions","networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: external-dns-viewer
  labels:
    app.kubernetes.io/name: external-dns
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns
subjects:
  - kind: ServiceAccount
    name: external-dns
    namespace: kube-system # change to desired namespace: externaldns, kube-addons
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
  labels:
    app.kubernetes.io/name: external-dns
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app.kubernetes.io/name: external-dns
  template:
    metadata:
      labels:
        app.kubernetes.io/name: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
        - name: external-dns
          image: registry.k8s.io/external-dns/external-dns:v0.15.0
          args:
            - --source=service
            - --source=ingress
            - --domain-filter=${MyDomain} # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
            - --provider=aws
            #- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
            - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
            - --registry=txt
            - --txt-owner-id=${MyDnzHostedZoneId}
          env:
            - name: AWS_DEFAULT_REGION
              value: ap-northeast-2 # change to region where EKS is installed

 

여기서 가장 중요한 부분은 containers에서 args 부분에 결국 해당 정보가 들어가는 것을 볼 수 있다.

먼저 external-dns는 결국 IRSA를 통해서 AWS와 연결되는 역할이어야 한다. 그래야 Route53에 직접 등록이 가능하기 때문이다.

또한 domain-filter와 txt-owner-id에 호스트 존을 입력해서 명확하게 어떤 DNS에 대해 필터할지 명확하게 해준다.

      serviceAccountName: external-dns
      containers:
        - name: external-dns
          image: registry.k8s.io/external-dns/external-dns:v0.15.0
          args:
            - --source=service
            - --source=ingress
            - --domain-filter=${MyDomain} # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
            - --provider=aws
            #- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
            - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
            - --registry=txt
            - --txt-owner-id=${MyDnzHostedZoneId}

 

생성되는 정보는 다음과 같다.

즉 external-dns라는 deployment가 담당하고 여기서 사용할 serviceaccount와 연결할 역할을 만든다.

serviceaccount/external-dns created
clusterrole.rbac.authorization.k8s.io/external-dns created
clusterrolebinding.rbac.authorization.k8s.io/external-dns-viewer created
deployment.apps/external-dns created

 

이제 파드 로그를 확인해보면 다음과 같이 도메인 필터가 동작하는 것을 확인할 수 있다.

보면 1분에 한번씩 새로고침되어 식별한다.

아래 yaml을 통해서 테트리스 예시를 배포해보자.

cat <<EOF | kubectl apply -f -
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
EOF

 

이제 가장 중요한것은 annotate를 통해서 tetris라는 서비스에 내 도메인 정보를 입력한다.

kubectl annotate service tetris "external-dns.alpha.kubernetes.io/hostname=tetris.$MyDomain"
while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done

 

자동으로 도메인이 등록된 것을 확인할 수 있다.

 

참고로 이 을 통해서 서비스가 삭제될 경우 자동으로 Route53에서 삭제하는 옵션도 존재한다.

이는 주로 테스트에서 쓰일 수 있다.

- --policy=upsert-only