들어가며

안녕하세요. 이번 주에는 AWS EKS의 VPC CNI에 대해 알아보겠습니다. KANS 3기 9주차 스터디를 시작하겠습니다.


AWS EKS : VPC CNI

AWS VPC CNI 소개

  • AWS VPC CNI는 AWS에서 제공하는 CNI(Container Network Interface) 플러그인으로, AWS VPC(Virtual Private Cloud)의 네트워크를 사용하여 파드 간 통신을 지원하는 CNI 플러그인입니다.
  • AWS VPC CNI의 특징
    • 가장 큰 특징은 파드의 IP 네트워크 대역과 노드(인스턴스)의 IP 네트워크 대역이 같아서 직접 통신이 가능하다는 것입니다. - Github, Proposal
    • 또한 VPC와 통합되어 VPC Flow logs나 VPC 라우팅 정책, 보안 그룹(Security Group) 등을 활용할 수 있습니다.
    • 아래에서 설명할 Warm Pool을 통해 노드의 IP 주소를 재사용하여 파드의 생성과 삭제를 빠르게 할 수 있습니다.
  • Amazon VPC CNI는 크게 두가지 구성요소로 이루어집니다.
    • CNI 바이너리 : 파드간 통신을 활성화 하도록 파드 네트워크를 설정합니다. CNI 바이너리는 노드의 루트 파일 시스템에서 실행되며, 새로운 파드가 추가되거나, 기존 파드가 제거 될때 kubelet에 의해 호출됩니다.
    • ipamd : IP 주소를 할당하고 관리하는 데몬으로 다음을 담당 합니다.
      • 노드에서 ENI(Elastic Network Interface - EC2에서 사용하는 일종의 가상 랜카드) 관리
      • 사용가능한 IP 주소들의 웜풀 유지
  • 인스턴스가 생성되면 EC2는 기본 서브넷과 연결된 기본 ENI를 생성하고 연결합니다. 호스트 네트워크 모드에서 실행되는 포드는 노드 기본 ENI에 할당된 기본 IP 주소를 사용하며 호스트와 동일한 네트워크 네임스페이스를 공유합니다.

웜풀(warm pool)은 노드가 프로비저닝 될 때 미리 할당된 IP 주소들의 집합입니다. 이것은 노드가 프로비저닝 될 때마다 새로운 IP 주소를 할당하는 것을 방지하고, 노드가 삭제되어도 IP 주소를 재사용할 수 있도록 하여 조금 더 빠른 파드의 생성과 삭제를 가능하게 합니다.

  • EC2 인스턴스별 최대 사용가능한 ENI 갯수는 인스턴스 타입에 따라 다르며, 인스턴스 타입별 ENI 갯수는 여기에서 확인할 수 있습니다. 이 ENI 갯수에 따라 할당 가능한 IP 주소의 갯수가 결정되며, 이에 따라 파드의 갯수가 제한될 수 있습니다.
  • 각 ENI는 할당할 수 있는 IP수가 제한되어 있으며, 이 수는 EC2 인스턴스 타입에 따라 다릅니다. 인스턴스 타입별 ENI 당 IP 할당 수는 여기에서 확인할 수 있습니다. ENI 당 할당할 수 있는 IP 를 slot 이라고 하며, 이 slot은 VPC CNI를 통해 파드에 할당됩니다.

    img.png 파드에 IP를 할당하기 위한 절차

Calico CNI와의 차이점

  • 노드와 파드의 네트워크 대역을 동일하게 설정함으로써 NAT(Network Address Translation)을 사용하지 않아도 되기 때문에 성능이 향상되고 지연이 최소화 됩니다.

img.png

  • 파드간 통신시 Calico CNI 등의 일반적인 CNI는 오버레이(VXLAN, IP-in-IP)를 사용하여 통신하지만, AWS VPC CNI는 VPC의 네트워크를 사용하여 직접 통신합니다.

img_1.png

IPv4 Prefix 위임 (Delegation)

  • 앞서 알아 본것 처럼 각 파드는 노드의 ENI에 할당된 IP 주소를 사용하고, ENI당 IP (slot) 수와 ENI 갯수는 인스턴스별로 제한이 있고, 이 값이 큰 편이 아닙니다. 이를 해결하기위해 IPv4 Prefix 위임이 도입되었습니다.
  • IPv4 Prefix는 ENI 별로 IP를 할당하지 않고, /28의 접두사 길이 CIDR 블록을 노드에 할당하여 파드에 할당할 IP 주소를 관리합니다. 이렇게 하면 각 슬랏당 16개의 IP주소를 할당할 수 있어서 다음과 같은 갯수의 최대 IP 또는 파드를 사용할 수 있습니다.
  • 최대 사용가능한 IP 수 계산 : 사용가능한 파드 IP 수 = (최대 네트워크 인터페이스(ENI) 갯수 * (네트워크 인터페이스 당 slot 수 - 1) * 16)
    • 단, 실제로는 인스턴스 유형별로 권장되는 최대 갯수로 선정됩니다.

img.png


실습 준비

구성 환경

  • 사전 준비물 : AWS 계정, SSH 키 페어, IAM 계정 생성 후 키
  • 전체 구성도 : VPC 1개(퍼블릭 서브넷 3개, 프라이빗 서브넷 3개), EKS 클러스터(Control Plane), 관리형 노드 그룹(EC2 3대), Add-on img.png
    • CloudFormation 스택 실행 시 파라미터를 기입하면, 해당 정보가 반영되어 배포됩니다.
    • 실습 환경을 위한 VPC 1개가 생성되고, 퍼블릭 서브넷 3개와 프라이빗 서브넷 3개가 생성됩니다.
    • CloudFormation 에 EC2의 UserData 부분(Script 실행)으로 Amazon EKS 설치(with OIDC, Endpoint Public)를 진행합니다.
    • 관리형 노드 그룹(워커 노드)는 AZ1~AZ3를 사용하여, 기본 3대로 구성됩니다
    • Add-on 같이 설치 됨 : 최신 버전 - kube-proxy, coredns, aws vpc cni - 링크
    • 노드EC2 IAM Profile 권한 추가 : external-dns-access, full-ecr-access, alb-ingress-access, awsLoadBalancerController

배포 및 테스트

  • 배포
    • aws cli를 설치하고, aws configure로 자격증명을 설정 후 아래의 명령을 실행합니다.
    • 다음의 파라미터가 있고, :point_right: 표시가 있는 부분은 필수로 설정해주어야 합니다.
      • Deploy EC2
        1. :point_right: KeyName : 작업용 bastion ec2에 SSH 접속을 위한 SSH 키페어 선택 ← 미리 SSH 키 생성 해두자!
        2. :point_right: MyIamUserAccessKeyID : 관리자 수준의 권한을 가진 IAM User의 액세스 키ID 입력
        3. :point_right: MyIamUserSecretAccessKey : 관리자 수준의 권한을 가진 IAM User의 시크릿 키ID 입력 ← 노출되지 않게 보안 주의
        4. :point_right: SgIngressSshCidr : 작업용 bastion ec2에 SSH 접속 가능한 IP 입력 (집 공인IP/32 입력), 보안그룹 인바운드 규칙에 반영됨
        5. MyInstanceType: 작업용 bastion EC2 인스턴스의 타입 (기본 t3.medium) ⇒ 변경 가능
        6. LatestAmiId : 작업용 bastion EC2에 사용할 AMI는 아마존리눅스2 최신 버전 사용
      • EKS Config
        1. ClusterBaseName : EKS 클러스터 이름이며, myeks 기본값 사용을 권장 → 이유: 실습 리소스 태그명과 실습 커멘드에서 사용
        2. KubernetesVersion : EKS 호환, 쿠버네티스 버전 (기본 v1.30, 실습은 1.30 버전 사용)
        3. WorkerNodeInstanceType: 워커 노드 EC2 인스턴스의 타입 (기본 t3.medium) ⇒ 변경 가능
        4. WorkerNodeCount : 워커노드의 갯수를 입력 (기본 3대) ⇒ 변경 가능
        5. WorkerNodeVolumesize : 워커노드의 EBS 볼륨 크기 (기본 80GiB) ⇒ 변경 가능
      • Region AZ : 리전과 가용영역을 지정, 기본값 그대로 사용
# YAML 파일 다운로드
$ curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/kans/eks-oneclick.yaml

# CloudFormation 스택 배포
# aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 MyIamUserAccessKeyID=<IAM User의 액세스키> MyIamUserSecretAccessKey=<IAM User의 시크릿 키> ClusterBaseName='<eks 이름>' --region ap-northeast-2
# 예시) aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=aws-ec2 SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks --region ap-northeast-2

## Tip. 워커노드 인스턴스 타입 변경 : WorkerNodeInstanceType=t3.xlarge
# 예시) aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=aws-ec2 SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks --region ap-northeast-2 WorkerNodeInstanceType=t3.xlarge 

$ aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks \
  --parameter-overrides KeyName=aws-ec2 \
  SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 \
  MyIamUserAccessKeyID=AKIA5... \
  MyIamUserSecretAccessKey='CVNa2...' \  
  ClusterBaseName=myeks \ 
  --region ap-northeast-2
# => Waiting for changeset to be created..
#    Waiting for stack create/update to complete
#    Successfully created/updated stack - myeks

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
$ aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text
# => 3.35.140.75

# 작업용 EC2 SSH 접속
$ ssh -i ~/.ssh/aws-ec2-key.cer ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
# =>    ,     #_
#       ~\_  ####_        Amazon Linux 2
#      ~~  \_#####\
#      ~~     \###|       AL2 End of Life is 2025-06-30.
#      ~~       \#/ ___
#       ~~       V~' '-&gt;
#        ~~~         /    A newer version of Amazon Linux is available!
#          ~~._.   _/
#             _/ _/       Amazon Linux 2023, GA and supported until 2028-03-15.
#           _/m/'           https://aws.amazon.com/linux/amazon-linux-2023/
#    
#    10 package(s) needed for security, out of 13 available
#    Run &quot;sudo yum update&quot; to apply all updates.
#    [root@myeks-bastion ~]#
  • 작업용 EC2에 SSH 키 파일 사용하여 SSH 접속 후 확인해보겠습니다.
  • 쿠버네티스 정상 설치 확인은 스택 생성 시작 후 20분 후 접속하는 것이 좋습니다.
  • 접속 후 기본 확인
# SSH 접속
$ ssh -i ~/.ssh/aws-ec2-key.cer ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

# cloud-init 실행 과정 로그 확인
$ tail -f /var/log/cloud-init-output.log
# =>  ...
#     69  dnsmasq                  available    [ =stable ]
#     70  unbound1.17              available    [ =stable ]
#     72  collectd-python3         available    [ =stable ]
#    † Note on end-of-support. Use 'info' subcommand.
#    Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.
#    cloudinit End!
#    Cloud-init v. 19.3-46.amzn2.0.2 finished at Sat, 01 Nov 2024 07:27:24 +0000. Datasource DataSourceEc2.  Up 84.79 seconds

# cloud-init 정상 완료 후 eksctl 실행 과정 로그 확인
$ tail -f /root/create-eks.log
# => ...
#    2024-10-01 16:26:46 [ℹ]  deploying stack &quot;eksctl-myeks-cluster&quot;
#    2024-10-01 16:27:16 [ℹ]  waiting for CloudFormation stack &quot;eksctl-myeks-cluster&quot;
#    ...
#    2024-10-01 16:40:31 [✔]  EKS cluster &quot;myeks&quot; in &quot;ap-northeast-2&quot; region is ready

# default 네임스페이스 적용
$ kubectl ns default
# => Context &quot;anonym@myeks.ap-northeast-2.eksctl.io&quot; modified.
#    Active namespace is &quot;default&quot;.

# 설치 확인
$ kubectl cluster-info
# => Kubernetes control plane is running at https://CF0FD5E1EEB937DD6FF881B7EBADDFD4.yl4.ap-northeast-2.eks.amazonaws.com
#    CoreDNS is running at https://CF0FD5E1EEB937DD6FF881B7EBADDFD4.yl4.ap-northeast-2.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
$ eksctl get cluster
# => NAME REGION    EKSCTL CREATED
#    myeks  ap-northeast-2  True
$ eksctl get nodegroup --cluster $CLUSTER_NAME
# => CLUSTER  NODEGROUP STATUS  CREATED               MIN SIZE  MAX SIZE  DESIRED CAPACITY  INSTANCE TYPE  IMAGE ID    ASG NAME                                      TYPE
#    myeks    ng1       ACTIVE  2024-10-01T07:37:58Z  3         3         3                 t3.medium      AL2_x86_64  eks-ng1-4ec975e7-9584-1403-37c4-fc55cc2ec860  managed

# 환경변수 정보 확인
$ export | egrep 'ACCOUNT|AWS_|CLUSTER|KUBERNETES|VPC|Subnet'
$ export | egrep 'ACCOUNT|AWS_|CLUSTER|KUBERNETES|VPC|Subnet' | egrep -v 'SECRET|KEY'
# => declare -x ACCOUNT_ID=&quot;123456789012&quot;
#    declare -x AWS_DEFAULT_REGION=&quot;ap-northeast-2&quot;
#    declare -x AWS_PAGER=&quot;&quot;
#    declare -x AWS_REGION=&quot;ap-northeast-2&quot;
#    declare -x CLUSTER_NAME=&quot;myeks&quot;
#    declare -x KUBERNETES_VERSION=&quot;1.30&quot;
#    declare -x PrivateSubnet1=&quot;subnet-02550e25cd3a9d814&quot;
#    declare -x PrivateSubnet2=&quot;subnet-0c2adfcc3d586e58d&quot;
#    declare -x PrivateSubnet3=&quot;subnet-04dd8e21b159de4cb&quot;
#    declare -x PubSubnet1=&quot;subnet-0a06ed52d587bd707&quot;
#    declare -x PubSubnet2=&quot;subnet-00b4dacf7eef35d33&quot;
#    declare -x PubSubnet3=&quot;subnet-03c911c48452b41b2&quot;
#    declare -x VPCID=&quot;vpc-0837921f515624150&quot;

# 인증 정보 확인
$ cat /root/.kube/config
$ kubectl config view
$ kubectl ctx
# => anonym@myeks.ap-northeast-2.eksctl.io

# 노드 정보 확인
$ kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
# => NAME                                               STATUS   ROLES    AGE     VERSION               INSTANCE-TYPE   CAPACITYTYPE   ZONE
#    ip-192-168-1-186.ap-northeast-2.compute.internal   Ready    &lt;none&gt;   7m26s   v1.30.4-eks-a737599   t3.medium       ON_DEMAND      ap-northeast-2a
#    ip-192-168-2-92.ap-northeast-2.compute.internal    Ready    &lt;none&gt;   7m22s   v1.30.4-eks-a737599   t3.medium       ON_DEMAND      ap-northeast-2b
#    ip-192-168-3-235.ap-northeast-2.compute.internal   Ready    &lt;none&gt;   7m22s   v1.30.4-eks-a737599   t3.medium       ON_DEMAND      ap-northeast-2c
$ eksctl get iamidentitymapping --cluster myeks
# => ARN                      USERNAME        GROUPS          ACCOUNT
#    arn:aws:iam::123456789012:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-kPdCWLD1sAsU  system:node: system:bootstrappers,system:nodes

# krew 플러그인 확인
$ kubectl krew list
# => PLUGIN   VERSION
#    ctx      v0.9.5
#    get-all  v1.3.8
#    krew     v0.4.4
#    neat     v2.0.4
#    ns       v0.9.5
#    stern    v1.31.0

# 모든 네임스페이스에서 모든 리소스 확인
$ kubectl get-all
  • 노드 접속 확인 및 SSH 접속
# 노드 IP 확인 및 PrivateIP 변수 지정
$ aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# => ------------------------------------------------------------------
#    |                        DescribeInstances                       |
#    +----------------+-----------------+------------------+----------+
#    |  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
#    +----------------+-----------------+------------------+----------+
#    |  myeks-ng1-Node|  192.168.2.92   |  43.203.143.233  |  running |
#    |  myeks-ng1-Node|  192.168.3.235  |  15.164.95.12    |  running |
#    |  myeks-bastion |  192.168.1.100  |  3.35.140.75     |  running |
#    |  myeks-ng1-Node|  192.168.1.186  |  3.38.183.82     |  running |
#    +----------------+-----------------+------------------+----------+
$ N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
$ N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2b -o jsonpath={.items[0].status.addresses[0].address})
$ N3=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
$ echo "export N1=$N1" >> /etc/profile
$ echo "export N2=$N2" >> /etc/profile
$ echo "export N3=$N3" >> /etc/profile
$ echo $N1, $N2, $N3
# => 192.168.1.186, 192.168.2.92, 192.168.3.235

# 보안그룹 ID와 보안그룹 이름(Name아님을 주의!) 확인
$ aws ec2 describe-security-groups --query 'SecurityGroups[*].[GroupId, GroupName]' --output text
# => sg-07b8900660cb85dff default
#    sg-0ece66fedd1fb4abe myeks-EKSEC2SG-1GNaTdbIbBNG
#    sg-07663468a536ba88b eksctl-myeks-cluster-ControlPlaneSecurityGroup-S7dWa32uw1S7
#    sg-0edc706b941f0aef7 eksctl-myeks-cluster-ClusterSharedNodeSecurityGroup-sO3DEJP4xewT
#    sg-02c614a038ec1f7e7 eks-cluster-sg-myeks-104368993
#    sg-036e220bf3d8aaf20 eksctl-myeks-nodegroup-ng1-remoteAccess
#    sg-035e2b98b5ac89231 default

# 노드 보안그룹 ID 확인
$ aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text
# => sg-036e220bf3d8aaf20
$ NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
$ echo $NGSGID
# => sg-036e220bf3d8aaf20
$ echo "export NGSGID=$NGSGID" >> /etc/profile

# 노드 보안그룹에 eksctl-host 에서 노드(파드)에 접속 가능하게 룰(Rule) 추가 설정
$ aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32

# eksctl-host 에서 노드의IP나 coredns 파드IP로 ping 테스트
$ ping -c 1 $N1
# => PING 192.168.1.186 (192.168.1.186) 56(84) bytes of data.
#    64 bytes from 192.168.1.186: icmp_seq=1 ttl=255 time=0.492 ms
#    
#    --- 192.168.1.186 ping statistics ---
#    1 packets transmitted, 1 received, 0% packet loss, time 0ms
#    rtt min/avg/max/mdev = 0.492/0.492/0.492/0.000 ms
$ ping -c 1 $N2
$ ping -c 1 $N3

# 워커 노드 SSH 접속 : '-i ~/.ssh/id_rsa' 생략 가능
$ for node in $N1 $N2 $N3; do ssh -o StrictHostKeyChecking=no ec2-user@$node hostname; done
$ ssh ec2-user@$N1
$ exit
$ ssh ec2-user@$N2
$ exit
$ ssh ec2-user@$N3
$ exit
  • Add-on 정보확인 : 최신 버전 - kube-proxy, coredns, aws vpc cni - 링크 mgmt
# 모든 파드의 컨테이너 이미지 정보 확인
$ kubectl get pods --all-namespaces -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort | uniq -c
# =>       3 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon/aws-network-policy-agent:v1.1.4-eksbuild.1
#          3 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon-k8s-cni:v1.18.6-eksbuild.1
#          2 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/eks/coredns:v1.11.3-eksbuild.2
#          3 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/eks/kube-proxy:v1.30.5-minimal-eksbuild.2
# 위 버전은 Add-on 으로 최신 버전 설치
$ kubectl get pods -A
$ kubectl get pods --all-namespaces -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort | uniq -c
# =>       3 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon/aws-network-policy-agent:v1.1.4-eksbuild.1
#          3 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/amazon-k8s-cni:v1.18.6-eksbuild.1
#          2 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/eks/coredns:v1.11.3-eksbuild.2
#          3 602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/eks/kube-proxy:v1.30.5-minimal-eksbuild.2

# eksctl 설치/업데이트 addon 확인
$ eksctl get addon --cluster $CLUSTER_NAME
# => NAME        VERSION             STATUS  ISSUES      IAMROLE                                                                       UPDATE                AVAILABLE  CONFIGURATION  VALUES  POD      IDENTITY  ASSOCIATION  ROLES
#    coredns     v1.11.3-eksbuild.2  ACTIVE  0
#    kube-proxy  v1.30.5-eksbuild.2  ACTIVE  0
#    vpc-cni     v1.18.6-eksbuild.1  ACTIVE  0           arn:aws:iam::123456789012:role/eksctl-myeks-addon-vpc-cni-Role1-QUy8qUBqFjcx  enableNetworkPolicy:  &quot;true&quot;

# (참고) eks 설치 yaml 중 addon 내용
$ tail -n11 myeks.yaml
# => addons:
#      - name: vpc-cni # no version is specified so it deploys the default version
#        version: latest # auto discovers the latest available
#        attachPolicyARNs:
#          - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
#        configurationValues: |-
#          enableNetworkPolicy: &quot;true&quot;
#      - name: kube-proxy
#        version: latest
#      - name: coredns
#        version: latest

노드에서 기본 네트워크 정보 확인

워커 노드 기본 네트워크 구성

img.png

  • 네트워크 네임스페이스는 호스트(Root)와 파드별(Per Pod)로 구분됩니다.
  • 특정한 파드 (kube-proxy, aws-node)는 호스트의 IP를 그대로 사용합니다. => 파드의 Host Network 옵션 - 참고
  • t3.medium 의 경우 ENI 마다 최대 6개의 IP를 가질 수 있습니다. (ENI 당 5개의 보조 IP)
  • ENI0, ENI1 으로 2개의 ENI는 자신의 IP 이외에 추가적으로 5개의 보조 프라이빗 IP를 가질 수 있습니다.
  • coredns 파드는 veth으로 호스트에는 eniY@ifN 인터페이스와 파드에 eth0과 연결되어 있습니다.

[실습] 보조 IPv4 주소를 파드가 사용하는지 확인

# coredns 파드 IP 정보 확인
$ kubectl get pod -n kube-system -l k8s-app=kube-dns -owide
# => NAME                       READY   STATUS    RESTARTS   AGE   IP             NODE                                               NOMINATED NODE   READINESS GATES
#    coredns-699d8c5988-bvxtz   1/1     Running   0          18m   192.168.1.30   ip-192-168-1-186.ap-northeast-2.compute.internal   &lt;none&gt;           &lt;none&gt;
#    coredns-699d8c5988-vt68k   1/1     Running   0          18m   192.168.3.82   ip-192-168-3-235.ap-northeast-2.compute.internal   &lt;none&gt;           &lt;none&gt;

# 노드의 라우팅 정보 확인 >> EC2 네트워크 정보의 '보조 프라이빗 IPv4 주소'와 비교해보자
$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
# => &gt;&gt; node 192.168.1.186 &lt;&lt;
#    default via 192.168.1.1 dev eth0
#    169.254.169.254 dev eth0
#    192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.186
#    192.168.1.30 dev eni319ad74733c scope link
#    
#    &gt;&gt; node 192.168.2.92 &lt;&lt;
#    default via 192.168.2.1 dev eth0
#    169.254.169.254 dev eth0
#    192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.92
#    
#    &gt;&gt; node 192.168.3.235 &lt;&lt;
#    default via 192.168.3.1 dev eth0
#    169.254.169.254 dev eth0
#    192.168.3.0/24 dev eth0 proto kernel scope link src 192.168.3.235
#    192.168.3.82 dev enid438418b082 scope link

# <span style="color: green;">👉 노드의 IP와 파드의 IP가 같은 대역임을 확인 할 수 있습니다.</span>
# <span style="color: green;">    예를 들어서 노드 IP가 192.168.1.186인 경우 파드는 192.168.1.30으로</span>
# <span style="color: green;">    동일한 IP 대역으로, 보조 IPv4가 사용됨을 확인 할 수 있습니다.</span>

[실습] 테스트용 파드 생성 - nicolaka/netshoot

# [터미널1~3] 노드 모니터링
$ ssh ec2-user@$N1
# => [ec2-user@ip-192-168-1-186 ~]$
$ watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
# => 2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc
#     mq state UP mode DEFAULT group default qlen 1000
#        link/ether 02:35:26:fe:f6:2f brd ff:ff:ff:ff:ff:ff
#    3: eni319ad74733c@if3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt;
#    mtu 9001 qdisc noqueue state UP mode DEFAULT group defaul
#    t
#        link/ether 16:69:60:63:cf:f7 brd ff:ff:ff:ff:ff:ff li
#    nk-netns cni-a953612e-ef39-78ca-660c-2a1ecde16b39
#    4: eth1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc
#     mq state UP mode DEFAULT group default qlen 1000
#        link/ether 02:86:42:43:71:d1 brd ff:ff:ff:ff:ff:ff
#    
#    [ROUTE TABLE]
#    192.168.1.30    0.0.0.0         255.255.255.255 UH    0
#        0        0 eni319ad74733c

$ ssh ec2-user@$N2
# => [ec2-user@ip-192-168-2-92 ~]$
$ watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
# => 2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc
#     mq state UP mode DEFAULT group default qlen 1000
#        link/ether 06:bc:36:ed:7c:d3 brd ff:ff:ff:ff:ff:ff
#    
#    [ROUTE TABLE]

$ ssh ec2-user@$N3
# => [ec2-user@ip-192-168-3-235 ~]$
$ watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
# => 2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc
#     mq state UP mode DEFAULT group default qlen 1000
#        link/ether 0a:3e:a1:00:00:05 brd ff:ff:ff:ff:ff:ff
#    3: enid438418b082@if3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt;
#    mtu 9001 qdisc noqueue state UP mode DEFAULT group defaul
#    t
#        link/ether 02:50:3a:92:0e:1b brd ff:ff:ff:ff:ff:ff li
#    nk-netns cni-23569158-d846-4981-4105-ec08005e9b95
#    4: eth1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc
#     mq state UP mode DEFAULT group default qlen 1000
#        link/ether 0a:e8:ac:41:8c:ab brd ff:ff:ff:ff:ff:ff
#    
#    [ROUTE TABLE]
#    192.168.3.82    0.0.0.0         255.255.255.255 UH    0
#        0        0 enid438418b082

# 테스트용 파드 netshoot-pod 생성
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: netshoot-pod
spec:
  replicas: 3
  selector:
    matchLabels:
      app: netshoot-pod
  template:
    metadata:
      labels:
        app: netshoot-pod
    spec:
      containers:
      - name: netshoot-pod
        image: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF
# => deployment.apps/netshoot-pod created

# 파드 이름 변수 지정
$ PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].metadata.name})
$ PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].metadata.name})
$ PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[2].metadata.name})

# 파드 확인
$ kubectl get pod -o wide
# => NAME                            READY   STATUS    RESTARTS   AGE   IP              NODE                                               NOMINATED NODE   READINESS GATES
#    netshoot-pod-74b7555dc7-6qsvf   1/1     Running   0          29s   192.168.1.112   ip-192-168-1-186.ap-northeast-2.compute.internal   &lt;none&gt;           &lt;none&gt;
#    netshoot-pod-74b7555dc7-x6lxb   1/1     Running   0          29s   192.168.2.172   ip-192-168-2-92.ap-northeast-2.compute.internal    &lt;none&gt;           &lt;none&gt;
#    netshoot-pod-74b7555dc7-x7r96   1/1     Running   0          29s   192.168.3.246   ip-192-168-3-235.ap-northeast-2.compute.internal   &lt;none&gt;           &lt;none&gt;
$ kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# => NAME                            IP
#    netshoot-pod-74b7555dc7-6qsvf   192.168.1.112
#    netshoot-pod-74b7555dc7-x6lxb   192.168.2.172
#    netshoot-pod-74b7555dc7-x7r96   192.168.3.246

# 노드에 라우팅 정보 확인
$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
# => &gt; node 192.168.1.186 &lt;&lt;
#    default via 192.168.1.1 dev eth0
#    169.254.169.254 dev eth0
#    192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.186
#    192.168.1.30 dev eni319ad74733c scope link
#    192.168.1.112 dev eni7ecb7efa346 scope link  # <span style="color: green;">👉 추가된 신규 파드의 IPv4 보조 IP</span>
#    
#    &gt;&gt; node 192.168.2.92 &lt;&lt;
#    default via 192.168.2.1 dev eth0
#    169.254.169.254 dev eth0
#    192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.92
#    192.168.2.172 dev enif06a5cbfaaa scope link  # <span style="color: green;">👉 추가된 신규 파드의 IPv4 보조 IP</span>
#    
#    &gt;&gt; node 192.168.3.235 &lt;&lt;
#    default via 192.168.3.1 dev eth0
#    169.254.169.254 dev eth0
#    192.168.3.0/24 dev eth0 proto kernel scope link src 192.168.3.235
#    192.168.3.82 dev enid438418b082 scope link
#    192.168.3.246 dev eniea7f0ec96dd scope link  # <span style="color: green;">👉 추가된 신규 파드의 IPv4 보조 IP</span>
  • 파드가 생성되면, 워커 노드에 eniY@ifN 추가되고 라우팅 테이블에도 정보가 추가된것을 확인 할 수 있습니다.
  • 테스트용 파드 eniY 정보 확인 - 워커 노드 EC2
# 노드3에서 네트워크 인터페이스 정보 확인
$ ssh ec2-user@$N3
----------------
$ ip -br -c addr show
# => lo               UNKNOWN        127.0.0.1/8 ::1/128
#    eth0             UP             192.168.3.235/24 fe80::83e:a1ff:fe00:5/64
#    enid438418b082@if3 UP             fe80::50:3aff:fe92:e1b/64
#    eth1             UP             192.168.3.236/24 fe80::8e8:acff:fe41:8cab/64
#    eniea7f0ec96dd@if3 UP             fe80::8cd8:57ff:fee7:29c/64
$ ip -c link
# => 1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
#        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
#    2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc mq state UP mode DEFAULT group default qlen 1000
#        link/ether 0a:3e:a1:00:00:05 brd ff:ff:ff:ff:ff:ff
#    3: enid438418b082@if3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc noqueue state UP mode DEFAULT group default
#        link/ether 02:50:3a:92:0e:1b brd ff:ff:ff:ff:ff:ff link-netns cni-23569158-d846-4981-4105-ec08005e9b95
#    4: eth1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc mq state UP mode DEFAULT group default qlen 1000
#        link/ether 0a:e8:ac:41:8c:ab brd ff:ff:ff:ff:ff:ff
#    5: eniea7f0ec96dd@if3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc noqueue state UP mode DEFAULT group default
#        link/ether 8e:d8:57:e7:02:9c brd ff:ff:ff:ff:ff:ff link-netns cni-508d7216-7484-4497-a023-c6236435d535
$ ip -c addr
# => ...
#    2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc mq state UP group default qlen 1000
#        link/ether 0a:3e:a1:00:00:05 brd ff:ff:ff:ff:ff:ff
#        inet 192.168.3.235/24 brd 192.168.3.255 scope global dynamic eth0
#           valid_lft 2166sec preferred_lft 2166sec
#        inet6 fe80::83e:a1ff:fe00:5/64 scope link
#           valid_lft forever preferred_lft forever
#    3: enid438418b082@if3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc noqueue state UP group default
#        link/ether 02:50:3a:92:0e:1b brd ff:ff:ff:ff:ff:ff link-netns cni-23569158-d846-4981-4105-ec08005e9b95
#        inet6 fe80::50:3aff:fe92:e1b/64 scope link
#           valid_lft forever preferred_lft forever
#    4: eth1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc mq state UP group default qlen 1000
#        link/ether 0a:e8:ac:41:8c:ab brd ff:ff:ff:ff:ff:ff
#        inet 192.168.3.236/24 brd 192.168.3.255 scope global eth1
#           valid_lft forever preferred_lft forever
#        inet6 fe80::8e8:acff:fe41:8cab/64 scope link
#           valid_lft forever preferred_lft forever
#    5: eniea7f0ec96dd@if3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc noqueue state UP group default
#        link/ether 8e:d8:57:e7:02:9c brd ff:ff:ff:ff:ff:ff link-netns cni-508d7216-7484-4497-a023-c6236435d535
#        inet6 fe80::8cd8:57ff:fee7:29c/64 scope link
#           valid_lft forever preferred_lft forever
$ ip route # 혹은 route -n
# => default via 192.168.3.1 dev eth0
#    169.254.169.254 dev eth0
#    192.168.3.0/24 dev eth0 proto kernel scope link src 192.168.3.235
#    192.168.3.82 dev enid438418b082 scope link
#    192.168.3.246 dev eniea7f0ec96dd scope link
  
# 마지막 생성된 네임스페이스 정보 출력 -t net(네트워크 타입)
$ sudo lsns -o PID,COMMAND -t net | awk 'NR>2 {print $1}' | tail -n 1
# => 9774
  
# 마지막 생성된 네임스페이스 net PID 정보 출력 -t net(네트워크 타입)를 변수 지정
$ MyPID=$(sudo lsns -o PID,COMMAND -t net | awk 'NR>2 {print $1}' | tail -n 1)
  
# PID 정보로 파드 정보 확인
$ sudo nsenter -t $MyPID -n ip -c addr
# => ...
#    3: eth0@if5: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc noqueue state UP group default
#        link/ether 6a:d5:4d:a7:45:7e brd ff:ff:ff:ff:ff:ff link-netnsid 0
#        inet 192.168.3.246/32 scope global eth0
#           valid_lft forever preferred_lft forever
#        inet6 fe80::68d5:4dff:fea7:457e/64 scope link
#           valid_lft forever preferred_lft forever
$ sudo nsenter -t $MyPID -n ip -c route
# => default via 169.254.1.1 dev eth0
#    169.254.1.1 dev eth0 scope link
  
$ exit
----------------
  • 테스트용 파드 접속(exec) 후 확인
# 테스트용 파드 접속(exec) 후 Shell 실행
$ kubectl exec -it $PODNAME1 -- zsh
# =>  netshoot-pod-74b7555dc7-6qsvf  ~ 
  
# 아래부터는 pod-1 Shell 에서 실행 : 네트워크 정보 확인
----------------------------
$ ip -c addr
# => ...
#    3: eth0@if5: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc noqueue state UP group default
#        link/ether c6:cb:41:a0:85:57 brd ff:ff:ff:ff:ff:ff link-netnsid 0
#        inet 192.168.1.112/32 scope global eth0
#           valid_lft forever preferred_lft forever
#        inet6 fe80::c4cb:41ff:fea0:8557/64 scope link
#           valid_lft forever preferred_lft forever
$ ip -c route
# => default via 169.254.1.1 dev eth0
#    169.254.1.1 dev eth0 scope link
$ route -n
# => Kernel IP routing table
#    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
#    0.0.0.0         169.254.1.1     0.0.0.0         UG    0      0        0 eth0
#    169.254.1.1     0.0.0.0         255.255.255.255 UH    0      0        0 eth0
#$ ping -c 1 <pod-2 IP>
$ ping -c 1 192.168.2.172
# => PING 192.168.2.172 (192.168.2.172) 56(84) bytes of data.
#    64 bytes from 192.168.2.172: icmp_seq=1 ttl=125 time=1.25 ms
#    
#    --- 192.168.2.172 ping statistics ---
#    1 packets transmitted, 1 received, 0% packet loss, time 0ms
#    rtt min/avg/max/mdev = 1.249/1.249/1.249/0.000 ms
$ ps
# => PID   USER     TIME  COMMAND
#        1 root      0:00 tail -f /dev/null
#      109 root      0:00 zsh
#      186 root      0:00 ps
$ cat /etc/resolv.conf
# => search default.svc.cluster.local svc.cluster.local cluster.local ap-northeast-2.compute.internal
#    nameserver 10.100.0.10
#    options ndots:5
$ exit
----------------------------
  
# 파드2 Shell 실행
$ kubectl exec -it $PODNAME2 -- ip -c addr
# => ...
#    3: eth0@if3: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 9001 qdisc noqueue state UP group default
#        link/ether 02:ea:11:63:f6:9f brd ff:ff:ff:ff:ff:ff link-netnsid 0
#        inet 192.168.2.172/32 scope global eth0
#           valid_lft forever preferred_lft forever
#        inet6 fe80::ea:11ff:fe63:f69f/64 scope link
#           valid_lft forever preferred_lft forever
  
# 파드3 Shell 실행
$ kubectl exec -it $PODNAME3 -- ip -br -c addr
# => lo               UNKNOWN        127.0.0.1/8 ::1/128
#    eth0@if5         UP             192.168.3.246/32 fe80::68d5:4dff:fea7:457e/64

노드간 파드 통신

  • 파드간 통신 흐름 : AWS VPC CNI의 경우 별도의 오버레이(Overlay) 통신 기술없이, VPC에서 Native하게 파드간 직접 통신이 가능합니다.

img.png

  • 파드간 통신 과정 참고

img.png https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md

[실습] 파드간 통신 테스트 및 확인 : 별도의 NAT 동작 없이 통신 가능!

# 파드 IP 변수 지정
$ PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].status.podIP})
$ PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].status.podIP})
$ PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[2].status.podIP})

# 파드1 Shell 에서 파드2로 ping 테스트
$ kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
# => PING 192.168.2.172 (192.168.2.172) 56(84) bytes of data.
#    64 bytes from 192.168.2.172: icmp_seq=1 ttl=125 time=0.915 ms
#    64 bytes from 192.168.2.172: icmp_seq=2 ttl=125 time=0.870 ms
#    
#    --- 192.168.2.172 ping statistics ---
#    2 packets transmitted, 2 received, 0% packet loss, time 1001ms
#    rtt min/avg/max/mdev = 0.870/0.892/0.915/0.022 ms

# 파드2 Shell 에서 파드3로 ping 테스트
$ kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP3
# => PING 192.168.3.246 (192.168.3.246) 56(84) bytes of data.
#    64 bytes from 192.168.3.246: icmp_seq=1 ttl=125 time=1.79 ms
#    64 bytes from 192.168.3.246: icmp_seq=2 ttl=125 time=1.25 ms
#    
#    --- 192.168.3.246 ping statistics ---
#    2 packets transmitted, 2 received, 0% packet loss, time 1002ms
#    rtt min/avg/max/mdev = 1.245/1.516/1.788/0.271 ms

# 파드3 Shell 에서 파드1로 ping 테스트
$ kubectl exec -it $PODNAME3 -- ping -c 2 $PODIP1
# => PING 192.168.1.112 (192.168.1.112) 56(84) bytes of data.
#    64 bytes from 192.168.1.112: icmp_seq=1 ttl=125 time=1.08 ms
#    64 bytes from 192.168.1.112: icmp_seq=2 ttl=125 time=1.23 ms
#    
#    --- 192.168.1.112 ping statistics ---
#    2 packets transmitted, 2 received, 0% packet loss, time 1001ms
#    rtt min/avg/max/mdev = 1.084/1.155/1.227/0.071 ms

# 워커 노드 EC2 : TCPDUMP 확인
## For Pod to external (outside VPC) traffic, we will program iptables to SNAT using Primary IP address on the Primary ENI.
$ sudo tcpdump -i any -nn icmp
# => 08:11:33.865634 IP 192.168.2.172 &gt; 192.168.3.246: ICMP echo request, id 19, seq 1, length 64
#    08:11:33.865684 IP 192.168.2.172 &gt; 192.168.3.246: ICMP echo request, id 19, seq 1, length 64
#    08:11:33.867117 IP 192.168.3.246 &gt; 192.168.2.172: ICMP echo reply, id 19, seq 1, length 64
#    08:11:33.867195 IP 192.168.3.246 &gt; 192.168.2.172: ICMP echo reply, id 19, seq 1, length 64
#    08:11:34.866610 IP 192.168.2.172 &gt; 192.168.3.246: ICMP echo request, id 19, seq 2, length 64
#    08:11:34.866654 IP 192.168.2.172 &gt; 192.168.3.246: ICMP echo request, id 19, seq 2, length 64
#    08:11:34.867865 IP 192.168.3.246 &gt; 192.168.2.172: ICMP echo reply, id 19, seq 2, length 64
#    08:11:34.867891 IP 192.168.3.246 &gt; 192.168.2.172: ICMP echo reply, id 19, seq 2, length 64
$ sudo tcpdump -i eth1 -nn icmp
# <span style="color: green;">👉 캡쳐된 패킷이 없습니다.</span>
$ sudo tcpdump -i eth0 -nn icmp
# => 08:11:33.865689 IP 192.168.2.172 &gt; 192.168.3.246: ICMP echo request, id 19, seq 1, length 64
#    08:11:33.867117 IP 192.168.3.246 &gt; 192.168.2.172: ICMP echo reply, id 19, seq 1, length 64
#    08:11:34.866654 IP 192.168.2.172 &gt; 192.168.3.246: ICMP echo request, id 19, seq 2, length 64
#    08:11:34.867865 IP 192.168.3.246 &gt; 192.168.2.172: ICMP echo reply, id 19, seq 2, length 64
$ sudo tcpdump -i eniYYYYYYYY -nn icmp
# => 08:11:33.865643 IP 192.168.2.172 &gt; 192.168.3.246: ICMP echo request, id 19, seq 1, length 64
#    08:11:33.867195 IP 192.168.3.246 &gt; 192.168.2.172: ICMP echo reply, id 19, seq 1, length 64
#    08:11:34.866610 IP 192.168.2.172 &gt; 192.168.3.246: ICMP echo request, id 19, seq 2, length 64
#    08:11:34.867891 IP 192.168.3.246 &gt; 192.168.2.172: ICMP echo reply, id 19, seq 2, length 64

# [워커 노드1]
# routing policy database management 확인
$ ip rule
# => 0: from all lookup local
#    512: from all to 192.168.1.30 lookup main
#    512: from all to 192.168.1.112 lookup main
#    1024:  from all fwmark 0x80/0x80 lookup main
#    32766: from all lookup main
#    32767: from all lookup default

# routing table management 확인
$ ip route show table local
# => broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
#    local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
#    local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
#    broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
#    broadcast 192.168.1.0 dev eth0 proto kernel scope link src 192.168.1.186
#    broadcast 192.168.1.0 dev eth1 proto kernel scope link src 192.168.1.9
#    local 192.168.1.9 dev eth1 proto kernel scope host src 192.168.1.9
#    local 192.168.1.186 dev eth0 proto kernel scope host src 192.168.1.186
#    broadcast 192.168.1.255 dev eth0 proto kernel scope link src 192.168.1.186
#    broadcast 192.168.1.255 dev eth1 proto kernel scope link src 192.168.1.9

# 디폴트 네트워크 정보를 eth0 을 통해서 빠져나간다
$ ip route show table main
# => default via 192.168.1.1 dev eth0
#    ...

파드에서 외부 통신

  • 파드에서 외부 통신 흐름 : iptable 에 SNAT 을 통하여 노드의 eth0 IP로 변경되어서 외부와 통신합니다.

img.png https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md

  • VPC CNI 의 External source network address translation (SNAT) 설정에 따라, 외부(인터넷) 통신 시 SNAT 하거나 혹은 SNAT 없이 통신을 할 수 있다 - 링크

[실습] 파드에서 외부 통신 테스트 및 확인

  • 파드 shell 실행 후 외부로 ping 테스트 & 워커 노드에서 tcpdump 및 iptables 정보 확인
# 작업용 EC2 : pod-1 Shell 에서 외부로 ping
$ kubectl exec -it $PODNAME1 -- ping -c 1 www.google.com
# => PING www.google.com (172.217.25.164) 56(84) bytes of data.
#    64 bytes from kix06s19-in-f4.1e100.net (172.217.25.164): icmp_seq=1 ttl=104 time=39.0 ms
#    
#    --- www.google.com ping statistics ---
#    1 packets transmitted, 1 received, 0% packet loss, time 0ms
#    rtt min/avg/max/mdev = 38.967/38.967/38.967/0.000 ms
$ kubectl exec -it $PODNAME1 -- ping -i 0.1 www.google.com
  
# 워커 노드 EC2 : TCPDUMP 확인
$ sudo tcpdump -i any -nn icmp
$ sudo tcpdump -i eth0 -nn icmp
  
# 작업용 EC2 : 퍼블릭IP 확인
$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s ipinfo.io/ip; echo; echo; done
  
# 작업용 EC2 : pod-1 Shell 에서 외부 접속 확인 - 공인IP는 어떤 주소인가?
## The right way to check the weather - [링크](https://github.com/chubin/wttr.in)
$ for i in $PODNAME1 $PODNAME2 $PODNAME3; do echo ">> Pod : $i <<"; kubectl exec -it $i -- curl -s ipinfo.io/ip; echo; echo; done
$ kubectl exec -it $PODNAME1 -- curl -s wttr.in/seoul
$ kubectl exec -it $PODNAME1 -- curl -s wttr.in/seoul?format=3
$ kubectl exec -it $PODNAME1 -- curl -s wttr.in/Moon
$ kubectl exec -it $PODNAME1 -- curl -s wttr.in/:help
  
# 워커 노드 EC2
$ ip rule
$ ip route show table main
$ sudo iptables -L -n -v -t nat
$ sudo iptables -t nat -S
  
# 파드가 외부와 통신시에는 아래 처럼 'AWS-SNAT-CHAIN-0' 룰(rule)에 의해서 SNAT 되어서 외부와 통신!
# 참고로 뒤 IP는 eth0(ENI 첫번째)의 IP 주소입니다.
# --random-fully 동작 - [링크1](https://ssup2.github.io/issue/Linux_TCP_SYN_Packet_Drop_SNAT_Port_Race_Condition/)  [링크2](https://ssup2.github.io/issue/Kubernetes_TCP_Connection_Delay_VXLAN_CNI_Plugin/)
$ sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
# => -A AWS-SNAT-CHAIN-0 -d 192.168.0.0/16 -m comment --comment &quot;AWS SNAT CHAIN&quot; -j RETURN
#    -A AWS-SNAT-CHAIN-0 ! -o vlan+ -m comment --comment &quot;AWS, SNAT&quot; -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.2.92 --random-fully
  
## 아래 'mark 0x4000/0x4000' 매칭되지 않아서 RETURN 됨!
# => -A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
#    -A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
#    -A KUBE-POSTROUTING -m comment --comment &quot;kubernetes service traffic requiring SNAT&quot; -j MASQUERADE --random-fully
#    ...
  
# 카운트 확인 시 AWS-SNAT-CHAIN-0에 매칭되어, 목적지가 192.168.0.0/16 아니고 외부 빠져나갈때 SNAT 192.168.1.251(EC2 노드1 IP) 변경되어 나갑니다.
$ sudo iptables -t filter --zero; sudo iptables -t nat --zero; sudo iptables -t mangle --zero; sudo iptables -t raw --zero
$ watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'
# => Chain AWS-SNAT-CHAIN-0 (1 references)
#     pkts bytes target     prot opt in     out     source               destination
#       12  1106 RETURN     all  --  *      *       0.0.0.0/0            192.168.0.0/16  /* AWS SNAT CHAIN */
#       31  1924 SNAT       all  --  *      !vlan+  0.0.0.0/0            0.0.0.0/0            /* AWS, SNAT */ ADDRTYPE match dst-type !LOCAL to:192.168.2.92 random-fully
#    
#    Chain KUBE-POSTROUTING (1 references)
#     pkts bytes target     prot opt in     out     source               destination
#       53  3630 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match ! 0x4000/0x4000
#        0     0 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK xor 0x4000
#        0     0 MASQUERADE  all  --  * * 0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ random-fully
#    
#    Chain POSTROUTING (policy ACCEPT 22 packets, 1706 bytes)
#     pkts bytes target     prot opt in     out     source               destination
#       53  3630 KUBE-POSTROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */
#       53  3630 AWS-SNAT-CHAIN-0  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* AWS SNAT CHAIN */
  
# conntrack 확인
$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo conntrack -L -n |grep -v '169.254.169'; echo; done
# => &gt;&gt; node 192.168.1.186 &lt;&lt;
#    udp      17 29 src=192.168.1.186 dst=146.56.40.151 sport=50772 dport=123 src=146.56.40.151 dst=192.168.1.186 sport=123 dport=47629 mark=128 use=1
#    tcp      6 86395 ESTABLISHED src=192.168.1.186 dst=52.95.197.175 sport=45832 dport=443 src=52.95.197.175 dst=192.168.1.186 sport=443 dport=30346 [ASSURED] mark=128 use=1
#    tcp      6 86395 ESTABLISHED src=192.168.1.186 dst=52.95.195.121 sport=48862 dport=443 src=52.95.195.121 dst=192.168.1.186 sport=443 dport=41664 [ASSURED] mark=128 use=1
#    conntrack v1.4.4 (conntrack-tools): 59 flow entries have been shown.
#    
#    &gt;&gt; node 192.168.2.92 &lt;&lt;
#    conntrack v1.4.4 (conntrack-tools): 50 flow entries have been shown.
#    
#    &gt;&gt; node 192.168.3.235 &lt;&lt;
#    conntrack v1.4.4 (conntrack-tools): 51 flow entries have been shown.
#    udp      17 23 src=192.168.3.235 dst=146.56.40.151 sport=51849 dport=123 src=146.56.40.151 dst=192.168.3.235 sport=123 dport=29777 mark=128 use=1
  • 다음 실습을 위해서 파드 삭제
$ kubectl delete deploy netshoot-pod
# => deployment.apps &quot;netshoot-pod&quot; deleted

노드의 파드 생성 갯수 제한

사전 준비 : kube-ops-view 설치

# kube-ops-view
$ helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
# => &quot;geek-cookbook&quot; has been added to your repositories
$ helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=LoadBalancer --set env.TZ="Asia/Seoul" --namespace kube-system
# => NAME: kube-ops-view
#    LAST DEPLOYED: Sat Oct  1 17:30:33 2024
#    NAMESPACE: kube-system
#    STATUS: deployed
#    REVISION: 1
#    TEST SUITE: None
#    NOTES:
#    1. Get the application URL by running these commands:
#         NOTE: It may take a few minutes for the LoadBalancer IP to be available.
#               You can watch the status of by running 'kubectl get svc -w kube-ops-view'
#      export SERVICE_IP=$(kubectl get svc --namespace kube-system kube-ops-view -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
#      echo http://$SERVICE_IP:8080

# kube-ops-view 접속 URL 확인 (1.5 배율)
$ kubectl get svc -n kube-system kube-ops-view -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "KUBE-OPS-VIEW URL = http://"$1":8080/#scale=1.5"}'
# => KUBE-OPS-VIEW URL = http://ae19b387d6fee48239f44d3f9f121378-262130081.ap-northeast-2.elb.amazonaws.com:8080/#scale=1.5

img.png

  • Secondary IPv4 addresses (기본값) : 인스턴스 유형에 최대 ENI 갯수와 할당 가능 IP 수를 조합하여 선정

워커 노드의 인스턴스 타입 별 파드 생성 갯수 제한

  • 인스턴스 타입 별 ENI 최대 갯수와 할당 가능한 최대 IP 갯수에 따라서 파드 배치 갯수가 결정됨
  • 단, aws-node 와 kube-proxy 파드는 호스트의 IP를 사용함으로 최대 갯수에서 제외함

img.png

👉 최대 파드 생성 갯수 : (Number of network interfaces for the instance type × (the number of IP addressess per network interface - 1)) + 2

워커 노드의 인스턴스 정보 확인 : t3.medium 사용 시

# t3 타입의 정보(필터) 확인
$ aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.* \
  --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
  --output table
# => --------------------------------------
#    |        DescribeInstanceTypes       |
#    +----------+----------+--------------+
#    | IPv4addr | MaxENI   |    Type      |
#    +----------+----------+--------------+
#    |  12      |  3       |  t3.large    |
#    |  6       |  3       |  t3.medium   |
#    |  15      |  4       |  t3.2xlarge  |
#    |  15      |  4       |  t3.xlarge   |
#    |  2       |  2       |  t3.micro    |
#    |  2       |  2       |  t3.nano     |
#    |  4       |  3       |  t3.small    |
#    +----------+----------+--------------+

# c5 타입의 정보(필터) 확인
$ aws ec2 describe-instance-types --filters Name=instance-type,Values=c5*.* \
  --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
  --output table
# => ----------------------------------------
#    |         DescribeInstanceTypes        |
#    +----------+----------+----------------+
#    | IPv4addr | MaxENI   |     Type       |
#    +----------+----------+----------------+
#    |  30      |  8       |  c5d.12xlarge  |
#    |  10      |  3       |  c5d.large     |
#    |  10      |  3       |  c5n.large     |
#    |  15      |  4       |  c5n.2xlarge   |
#    |  30      |  8       |  c5.4xlarge    |
#    |  15      |  4       |  c5a.xlarge    |
#    |  30      |  8       |  c5a.4xlarge   |
#    |  15      |  4       |  c5d.2xlarge   |
#    |  50      |  15      |  c5d.24xlarge  |
#    |  30      |  8       |  c5.12xlarge   |
#    |  30      |  8       |  c5n.9xlarge   |
#    |  30      |  8       |  c5a.12xlarge  |
#    |  15      |  4       |  c5.xlarge     |
#    |  30      |  8       |  c5n.4xlarge   |
#    |  50      |  15      |  c5n.18xlarge  |
#    |  50      |  15      |  c5.24xlarge   |
#    |  30      |  8       |  c5d.4xlarge   |
#    |  30      |  8       |  c5.9xlarge    |
#    |  15      |  4       |  c5a.2xlarge   |
#    |  50      |  15      |  c5.metal      |
#    |  50      |  15      |  c5a.24xlarge  |
#    |  50      |  15      |  c5d.18xlarge  |
#    |  50      |  15      |  c5a.16xlarge  |
#    |  30      |  8       |  c5a.8xlarge   |
#    |  50      |  15      |  c5n.metal     |
#    |  50      |  15      |  c5d.metal     |
#    |  10      |  3       |  c5.large      |
#    |  15      |  4       |  c5.2xlarge    |
#    |  15      |  4       |  c5d.xlarge    |
#    |  10      |  3       |  c5a.large     |
#    |  50      |  15      |  c5.18xlarge   |
#    |  30      |  8       |  c5d.9xlarge   |
#    |  15      |  4       |  c5n.xlarge    |
#    +----------+----------+----------------+  

# 파드 사용 가능 계산 예시 : aws-node 와 kube-proxy 파드는 host-networking 사용으로 IP 2개 남음
# ((MaxENI * (IPv4addr-1)) + 2)
# t3.medium 경우 : ((3 * (6 - 1) + 2 ) = 17개 >> aws-node 와 kube-proxy 2개 제외하면 15개

# 워커노드 상세 정보 확인 : 노드 상세 정보의 Allocatable 에 pods 에 17개 정보 확인
$ kubectl describe node | grep Allocatable: -A6
# => Allocatable:
#      cpu:                1930m
#      ephemeral-storage:  27905944324
#      hugepages-1Gi:      0
#      hugepages-2Mi:      0
#      memory:             3388304Ki
#      pods:               17
#    ...

최대 파드 생성 및 확인

# 워커 노드 EC2 - 모니터링
$ while true; do ip -br -c addr show && echo "--------------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done

# 작업용 EC2 - 터미널1
$ watch -d 'kubectl get pods -o wide'

# 작업용 EC2 - 터미널2
# 디플로이먼트 생성
$ curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/nginx-dp.yaml
$ kubectl apply -f nginx-dp.yaml
# => deployment.apps/nginx-deployment created

# 파드 확인
$ kubectl get pod -o wide
$ kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# => NAME                                IP
#    nginx-deployment-6f999cfffb-44rw7   192.168.1.230
#    nginx-deployment-6f999cfffb-xk4nh   192.168.3.246

$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -brief addr | wc -l ; echo; done
# => &gt;&gt; node 192.168.1.186 &lt;&lt;
#    5
#    &gt;&gt; node 192.168.2.92 &lt;&lt;
#    4
#    &gt;&gt; node 192.168.3.235 &lt;&lt;
#    5

# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인
$ kubectl scale deployment nginx-deployment --replicas=8
$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -brief addr | wc -l ; echo; done
# => &gt;&gt; node 192.168.1.186 &lt;&lt;
#    7
#    &gt;&gt; node 192.168.2.92 &lt;&lt;
#    6
#    &gt;&gt; node 192.168.3.235 &lt;&lt;
#    7

# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인 >> 어떤일이 벌어졌는가?
$ kubectl scale deployment nginx-deployment --replicas=15
$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -brief addr | wc -l ; echo; done
# => &gt;&gt; node 192.168.1.186 &lt;&lt;
#    9
#    &gt;&gt; node 192.168.2.92 &lt;&lt;
#    9
#    &gt;&gt; node 192.168.3.235 &lt;&lt;
#    10

# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인 >> 어떤일이 벌어졌는가?
$ kubectl scale deployment nginx-deployment --replicas=30
$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -brief addr | wc -l ; echo; done
# => &gt;&gt; node 192.168.1.186 &lt;&lt;
#    15
#    &gt;&gt; node 192.168.2.92 &lt;&lt;
#    15
#    &gt;&gt; node 192.168.3.235 &lt;&lt;
#    15

# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인 >> 어떤일이 벌어졌는가?
$ kubectl scale deployment nginx-deployment --replicas=50
$ for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -brief addr | wc -l ; echo; done
# => &gt;&gt; node 192.168.1.186 &lt;&lt;
#    19
#    &gt;&gt; node 192.168.2.92 &lt;&lt;
#    19
#    &gt;&gt; node 192.168.3.235 &lt;&lt;
#    19

# 파드 생성 실패!
$ kubectl get pods | grep Pending
# => nginx-deployment-6f999cfffb-59cj6   0/1     Pending   0          69s
#    nginx-deployment-6f999cfffb-5z5qg   0/1     Pending   0          69s
#    nginx-deployment-6f999cfffb-6gk4m   0/1     Pending   0          69s
#    nginx-deployment-6f999cfffb-bhzc8   0/1     Pending   0          69s
#    nginx-deployment-6f999cfffb-hlns8   0/1     Pending   0          69s
#    nginx-deployment-6f999cfffb-n64nc   0/1     Pending   0          69s
#    nginx-deployment-6f999cfffb-w8x7g   0/1     Pending   0          69s
#    nginx-deployment-6f999cfffb-zb2pk   0/1     Pending   0          69s

#$ kubectl describe pod <Pending 파드> | grep Events: -A5
$ kubectl describe pod nginx-deployment-6f999cfffb-59cj6 | grep Events: -A5
# => Events:
#      Type     Reason            Age   From               Message
#      ----     ------            ----  ----               -------
#      Warning  FailedScheduling  101s  default-scheduler  0/3 nodes are available: 3 Too many pods. preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.

img.png

# 디플로이먼트 삭제
$ kubectl delete deploy nginx-deployment
# => deployment.apps &quot;nginx-deployment&quot; deleted
  • 👉 해결 방안 : IPv4 Prefix Delegation, WARM & MIN IP/Prefix Targets, Custom Network

img.png IPv4 Prefix Delegation을 통한 IP 갯수 제한 해소


Service & AWS LoadBalancer Controller

서비스 종류

  • ClusterIP 타입

img.png

  • NodePort 타입

img_1.png

  • LoadBalancer 타입 (기본 모드) : NLB 인스턴스 유형

img_2.png

  • Service (LoadBalancer Controller) : AWS Load Balancer Controller + NLB IP 모드 동작 with AWS VPC CNI

img_3.png

NLB 모드 전체 정리

Instance mode

img_4.png https://aws.amazon.com/blogs/networking-and-content-delivery/deploying-aws-load-balancer-controller-on-amazon-eks/

  • externalTrafficPolicy에 따른 동작은 다음과 같습니다.
    • externalTrafficPolicy: ClusterIP : 2번 분산 및 SNAT으로 Client IP 확인 불가능합니다. <- LoadBalancer 타입 (기본 모드) 동작
    • externalTrafficPolicy: Local : 1번 분산 및 ClientIP가 유지되고, 워커 노드의 iptables을 사용합니다.
      • 통신 흐름 : 외부 클라이언트가 ‘로드밸런서’ 접속 시 부하분산 되어 노드 도달 후 iptables 룰로 목적지 파드와 통신됩니다.

        img_5.png externalTrafficPolicy: Local일때의 통신흐름의 예

        img_6.png

        • 노드는 외부에 공개되지 않고 로드밸런서만 외부에 공개되며, 외부 클라이언트는 로드밸랜서에 접속을 할 뿐 내부 노드의 정보를 알 수 없습니다.
        • 로드밸런서가 부하분산하여 파드가 존재하는 노드들에게 전달합니다. iptables 룰에서는 자신의 노드에 있는 파드만 연결합니다. (externalTrafficPolicy: local)
        • DNAT가 2번 동작합니다. (1) 로드밸런서 접속 후 빠져 나갈때, (2) 노드의 iptables 룰에서 파드IP 전달 시
        • 외부 클라이언트의 IP가 보존됩니다. AWS NLB 는 타켓인스턴스일 경우 클라이언트 IP를 유지, iptables 룰 경우도 externalTrafficPolicy: local 로 클라이언트 IP를 보존합니다.
  • 부하분산 최적화 : 노드에 파드가 없을 경우 ‘로드밸런서’에서 노드에 헬스 체크(상태 검사)가 실패하여 해당 노드로는 외부 요청 트래픽을 전달하지 않습니다.

    img_7.png img.png

    위의 이미지 처럼 3번째 인스턴스(Node3)은 상태 확인이 실패한 경우, 해당 노드로는 외부 요청 트래픽 전달하지 않습니다.

IP mode
  • IP 모드는 반드시 AWS LoadBalancer 컨트롤러 파드 및 정책 설정이 필요합니다.

    img_8.png https://aws.amazon.com/blogs/networking-and-content-delivery/deploying-aws-load-balancer-controller-on-amazon-eks/

    • Proxy Protocol v2 비활성화 ⇒ NLB에서 바로 파드로 인입되며, 단 ClientIP가 NLB로 SNAT 되어 Client IP 확인 불가능합니다.
    • Proxy Protocol v2 활성화 ⇒ NLB에서 바로 파드로 인입 및 ClientIP 확인 가능합니다. 단, PPv2를 애플리케이션이 인지할 수 있게 설정이 필요합니다.
  • AWS LoadBalancer Controller 배포 - Link

    # Helm Chart 설치
    $ helm repo add eks https://aws.github.io/eks-charts
    # => &quot;eks&quot; has been added to your repositories
    $ helm repo update
    # => Hang tight while we grab the latest from your chart repositories...
    #    ...Successfully got an update from the &quot;eks&quot; chart repository
    #    ...Successfully got an update from the &quot;geek-cookbook&quot; chart repository
    #    Update Complete. ⎈Happy Helming!⎈
    $ helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME
    # => NAME: aws-load-balancer-controller
    #    LAST DEPLOYED: Sat Oct  1 17:45:09 2024
    #    NAMESPACE: kube-system
    #    STATUS: deployed
    #    REVISION: 1
    #    TEST SUITE: None
    #    NOTES:
    #    AWS Load Balancer controller installed!
      
    ## 설치 확인
    $ kubectl get crd
    # => NAME                                         CREATED AT
    #    ...
    #    ingressclassparams.elbv2.k8s.aws             2024-10-01T08:45:08Z
    #    ...
    #    targetgroupbindings.elbv2.k8s.aws            2024-10-01T08:45:08Z
    $ kubectl get deployment -n kube-system aws-load-balancer-controller
    # => NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
    #    aws-load-balancer-controller   2/2     2            2           58s
    $ kubectl describe deploy -n kube-system aws-load-balancer-controller
    $ kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
    # =>   Service Account:  aws-load-balancer-controller
      
    # 클러스터롤, 롤 확인
    $ kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
    $ kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
    # => ...
    #    PolicyRule:
    #      Resources                                     Non-Resource URLs  Resource Names  Verbs
    #      ---------                                     -----------------  --------------  -----
    #      targetgroupbindings.elbv2.k8s.aws             []                 []              [create delete get list patch update watch]
    #      events                                        []                 []              [create patch]
    #      configmaps                                    []                 []              [get delete create update]
    #      ingresses                                     []                 []              [get list patch update watch]
    #      services                                      []                 []              [get list patch update watch]
    #      ingresses.extensions                          []                 []              [get list patch update watch]
    #      services.extensions                           []                 []              [get list patch update watch]
    #      ingresses.networking.k8s.io                   []                 []              [get list patch update watch]
    #      services.networking.k8s.io                    []                 []              [get list patch update watch]
    #      endpoints                                     []                 []              [get list watch]
    #      namespaces                                    []                 []              [get list watch]
    #      nodes                                         []                 []              [get list watch]
    #      pods                                          []                 []              [get list watch]
    #      endpointslices.discovery.k8s.io               []                 []              [get list watch]
    #      ingressclassparams.elbv2.k8s.aws              []                 []              [get list watch]
    #      ingressclasses.networking.k8s.io              []                 []              [get list watch]
    #      ingresses/status                              []                 []              [update patch]
    #      pods/status                                   []                 []              [update patch]
    #      services/status                               []                 []              [update patch]
    #      targetgroupbindings/status                    []                 []              [update patch]
    #      ingresses.elbv2.k8s.aws/status                []                 []              [update patch]
    #      pods.elbv2.k8s.aws/status                     []                 []              [update patch]
    #      services.elbv2.k8s.aws/status                 []                 []              [update patch]
    #      targetgroupbindings.elbv2.k8s.aws/status      []                 []              [update patch]
    #      ingresses.extensions/status                   []                 []              [update patch]
    #      pods.extensions/status                        []                 []              [update patch]
    #      services.extensions/status                    []                 []              [update patch]
    #      targetgroupbindings.extensions/status         []                 []              [update patch]
    #      ingresses.networking.k8s.io/status            []                 []              [update patch]
    #      pods.networking.k8s.io/status                 []                 []              [update patch]
    #      services.networking.k8s.io/status             []                 []              [update patch]
    #      targetgroupbindings.networking.k8s.io/status  []                 []              [update patch]
    
  • 서비스/파드 배포 테스트 with NLB - 링크 NLB

    # 모니터링
    $ watch -d kubectl get pod,svc,ep
      
    # 작업용 EC2 - 디플로이먼트 & 서비스 생성
    $ curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/echo-service-nlb.yaml
    $ cat echo-service-nlb.yaml
    $ kubectl apply -f echo-service-nlb.yaml
    # => deployment.apps/deploy-echo created
    #    service/svc-nlb-ip-type created
      
    # 확인
    $ kubectl get deploy,pod
    # => NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
    #    deployment.apps/deploy-echo   2/2     2            2           10s
    #    
    #    NAME                               READY   STATUS    RESTARTS   AGE
    #    pod/deploy-echo-857b6cfb88-452cd   1/1     Running   0          10s
    #    pod/deploy-echo-857b6cfb88-5xz5j   1/1     Running   0          10s
    $ kubectl get svc,ep,ingressclassparams,targetgroupbindings
    # => NAME                      TYPE           CLUSTER-IP      EXTERNAL-IP                                                                         PORT(S)        AGE
    #    service/kubernetes        ClusterIP      10.100.0.1      &lt;none&gt;                                                                              443/TCP        76m
    #    service/svc-nlb-ip-type   LoadBalancer   10.100.83.184   k8s-default-svcnlbip-50f5d03f41-f28c6aa861a2e815.elb.ap-northeast-2.amazonaws.com   80:30771/TCP   21s
    #    
    #    NAME                        ENDPOINTS                              AGE
    #    endpoints/kubernetes        192.168.1.65:443,192.168.3.189:443     76m
    #    endpoints/svc-nlb-ip-type   192.168.1.112:8080,192.168.2.24:8080   21s
    #    
    #    NAME                                   GROUP-NAME   SCHEME   IP-ADDRESS-TYPE   AGE
    #    ingressclassparams.elbv2.k8s.aws/alb                                           3m9s
    #    
    #    NAME                                                               SERVICE-NAME      SERVICE-PORT   TARGET-TYPE   AGE
    #    targetgroupbinding.elbv2.k8s.aws/k8s-default-svcnlbip-f58255c318   svc-nlb-ip-type   80             ip            16s
    $ kubectl get targetgroupbindings -o json | jq
    # => {
    #      &quot;apiVersion&quot;: &quot;v1&quot;,
    #      &quot;items&quot;: [
    #        {
    #          &quot;apiVersion&quot;: &quot;elbv2.k8s.aws/v1beta1&quot;,
    #          &quot;kind&quot;: &quot;TargetGroupBinding&quot;,
    #          &quot;metadata&quot;: {
    #            &quot;annotations&quot;: {
    #              &quot;elbv2.k8s.aws/checkpoint&quot;: &quot;gs98wrXMlQMdFGryEbrFbVngcODAXK0Yk4czpOdn9bg/biShKK1OQPD05qA040YQHH29qU6aPNq6J-fRu4M-dKY&quot;,
    #              &quot;elbv2.k8s.aws/checkpoint-timestamp&quot;: &quot;1730537287&quot;
    #            },
    #            &quot;creationTimestamp&quot;: &quot;2024-10-01T08:48:04Z&quot;,
    #            &quot;finalizers&quot;: [
    #              &quot;elbv2.k8s.aws/resources&quot;
    #            ],
    #            &quot;generation&quot;: 1,
    #            &quot;labels&quot;: {
    #              &quot;service.k8s.aws/stack-name&quot;: &quot;svc-nlb-ip-type&quot;,
    #              &quot;service.k8s.aws/stack-namespace&quot;: &quot;default&quot;
    #            },
    #            &quot;name&quot;: &quot;k8s-default-svcnlbip-f58255c318&quot;,
    #            &quot;namespace&quot;: &quot;default&quot;,
    #            &quot;resourceVersion&quot;: &quot;17369&quot;,
    #            &quot;uid&quot;: &quot;bdf37bcf-7fab-47ad-8b73-fb97c0239c9a&quot;
    #          },
    #          &quot;spec&quot;: {
    #            &quot;ipAddressType&quot;: &quot;ipv4&quot;,
    #            &quot;networking&quot;: {
    #              &quot;ingress&quot;: [
    #                {
    #                  &quot;from&quot;: [
    #                    {
    #                      &quot;securityGroup&quot;: {
    #                        &quot;groupID&quot;: &quot;sg-0a7a0dcdcda95c9fc&quot;
    #                      }
    #                    }
    #                  ],
    #                  &quot;ports&quot;: [
    #                    {
    #                      &quot;port&quot;: 8080,
    #                      &quot;protocol&quot;: &quot;TCP&quot;
    #                    }
    #                  ]
    #                }
    #              ]
    #            },
    #            &quot;serviceRef&quot;: {
    #              &quot;name&quot;: &quot;svc-nlb-ip-type&quot;,
    #              &quot;port&quot;: 80
    #            },
    #            &quot;targetGroupARN&quot;: &quot;arn:aws:elasticloadbalancing:ap-northeast-2:123456789012:targetgroup/k8s-default-svcnlbip-f58255c318/0cfa12d6c579f11b&quot;,
    #            &quot;targetType&quot;: &quot;ip&quot;,
    #            &quot;vpcID&quot;: &quot;vpc-0837921f515624150&quot;
    #          },
    #          &quot;status&quot;: {
    #            &quot;observedGeneration&quot;: 1
    #          }
    #        }
    #      ],
    #      &quot;kind&quot;: &quot;List&quot;,
    #      &quot;metadata&quot;: {
    #        &quot;resourceVersion&quot;: &quot;&quot;
    #      }
    #    }
      
    # (옵션) 빠른 실습을 위해서 등록 취소 지연(드레이닝 간격) 수정 : 기본값 300초
    $ vi echo-service-nlb.yaml
    ---
    ..
    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"
        service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: deregistration_delay.timeout_seconds=60
    ...
    :wq!
    ---
    $ kubectl apply -f echo-service-nlb.yaml
    # => deployment.apps/deploy-echo unchanged
    #    service/svc-nlb-ip-type configured
      
    # AWS ELB(NLB) 정보 확인
    $ aws elbv2 describe-load-balancers | jq
    # => {
    #      &quot;LoadBalancers&quot;: [
    #        {
    #          &quot;LoadBalancerArn&quot;: &quot;arn:aws:elasticloadbalancing:ap-northeast-2:123456789012:loadbalancer/net/k8s-default-svcnlbip-50f5d03f41/f28c6aa861a2e815&quot;,
    #          &quot;DNSName&quot;: &quot;k8s-default-svcnlbip-50f5d03f41-f28c6aa861a2e815.elb.ap-northeast-2.amazonaws.com&quot;,
    #          &quot;CanonicalHostedZoneId&quot;: &quot;ZIBE1TIR4HY56&quot;,
    #          &quot;CreatedTime&quot;: &quot;2024-10-01T08:48:03.532000+00:00&quot;,
    #          &quot;LoadBalancerName&quot;: &quot;k8s-default-svcnlbip-50f5d03f41&quot;,
    #          &quot;Scheme&quot;: &quot;internet-facing&quot;,
    #          &quot;VpcId&quot;: &quot;vpc-0837921f515624150&quot;,
    #          &quot;State&quot;: {
    #            &quot;Code&quot;: &quot;provisioning&quot;
    #          },
    #          &quot;Type&quot;: &quot;network&quot;,
    #          &quot;AvailabilityZones&quot;: [
    #            {
    #              &quot;ZoneName&quot;: &quot;ap-northeast-2a&quot;,
    #              &quot;SubnetId&quot;: &quot;subnet-0a06ed52d587bd707&quot;,
    #              &quot;LoadBalancerAddresses&quot;: []
    #            },
    #            {
    #              &quot;ZoneName&quot;: &quot;ap-northeast-2c&quot;,
    #              &quot;SubnetId&quot;: &quot;subnet-03c911c48452b41b2&quot;,
    #              &quot;LoadBalancerAddresses&quot;: []
    #            },
    #            {
    #              &quot;ZoneName&quot;: &quot;ap-northeast-2b&quot;,
    #              &quot;SubnetId&quot;: &quot;subnet-00b4dacf7eef35d33&quot;,
    #              &quot;LoadBalancerAddresses&quot;: []
    #            }
    #          ],
    #          &quot;SecurityGroups&quot;: [
    #            &quot;sg-0a7a0dcdcda95c9fc&quot;,
    #            &quot;sg-0e325a379a10ced56&quot;
    #          ],
    #          &quot;IpAddressType&quot;: &quot;ipv4&quot;,
    #          &quot;EnablePrefixForIpv6SourceNat&quot;: &quot;off&quot;
    #        }
    #      ]
    #    }
    $ aws elbv2 describe-load-balancers --query 'LoadBalancers[*].State.Code' --output text
    # => provisioning
    $ ALB_ARN=$(aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-default-svcnlbip`) == `true`].LoadBalancerArn' | jq -r '.[0]')
    $ aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN | jq
    # => {
    #      &quot;TargetGroups&quot;: [
    #        {
    #          &quot;TargetGroupArn&quot;: &quot;arn:aws:elasticloadbalancing:ap-northeast-2:123456789012:targetgroup/k8s-default-svcnlbip-f58255c318/0cfa12d6c579f11b&quot;,
    #          &quot;TargetGroupName&quot;: &quot;k8s-default-svcnlbip-f58255c318&quot;,
    #          &quot;Protocol&quot;: &quot;TCP&quot;,
    #          &quot;Port&quot;: 8080,
    #          &quot;VpcId&quot;: &quot;vpc-0837921f515624150&quot;,
    #          &quot;HealthCheckProtocol&quot;: &quot;TCP&quot;,
    #          &quot;HealthCheckPort&quot;: &quot;8080&quot;,
    #          &quot;HealthCheckEnabled&quot;: true,
    #          &quot;HealthCheckIntervalSeconds&quot;: 10,
    #          &quot;HealthCheckTimeoutSeconds&quot;: 10,
    #          &quot;HealthyThresholdCount&quot;: 3,
    #          &quot;UnhealthyThresholdCount&quot;: 3,
    #          &quot;LoadBalancerArns&quot;: [
    #            &quot;arn:aws:elasticloadbalancing:ap-northeast-2:123456789012:loadbalancer/net/k8s-default-svcnlbip-50f5d03f41/f28c6aa861a2e815&quot;
    #          ],
    #          &quot;TargetType&quot;: &quot;ip&quot;,
    #          &quot;IpAddressType&quot;: &quot;ipv4&quot;
    #        }
    #      ]
    #    }
    $ 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
    # => {
    #      &quot;TargetHealthDescriptions&quot;: [
    #        {
    #          &quot;Target&quot;: {
    #            &quot;Id&quot;: &quot;192.168.2.24&quot;,
    #            &quot;Port&quot;: 8080,
    #            &quot;AvailabilityZone&quot;: &quot;ap-northeast-2b&quot;
    #          },
    #          &quot;HealthCheckPort&quot;: &quot;8080&quot;,
    #          &quot;TargetHealth&quot;: {
    #            &quot;State&quot;: &quot;healthy&quot;
    #          },
    #          &quot;AdministrativeOverride&quot;: {
    #            &quot;State&quot;: &quot;no_override&quot;,
    #            &quot;Reason&quot;: &quot;AdministrativeOverride.NoOverride&quot;,
    #            &quot;Description&quot;: &quot;No override is currently active on target&quot;
    #          }
    #        },
    #        {
    #          &quot;Target&quot;: {
    #            &quot;Id&quot;: &quot;192.168.1.112&quot;,
    #            &quot;Port&quot;: 8080,
    #            &quot;AvailabilityZone&quot;: &quot;ap-northeast-2a&quot;
    #          },
    #          &quot;HealthCheckPort&quot;: &quot;8080&quot;,
    #          &quot;TargetHealth&quot;: {
    #            &quot;State&quot;: &quot;initial&quot;,
    #            &quot;Reason&quot;: &quot;Elb.InitialHealthChecking&quot;,
    #            &quot;Description&quot;: &quot;Initial health checks in progress&quot;
    #          },
    #          &quot;AdministrativeOverride&quot;: {
    #            &quot;State&quot;: &quot;no_override&quot;,
    #            &quot;Reason&quot;: &quot;AdministrativeOverride.NoOverride&quot;,
    #            &quot;Description&quot;: &quot;No override is currently active on target&quot;
    #          }
    #        }
    #      ]
    #    }
      
    # 웹 접속 주소 확인
    $ kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Pod Web URL = http://"$1 }'
    # => Pod Web URL = http://k8s-default-svcnlbip-50f5d03f41-f28c6aa861a2e815.elb.ap-northeast-2.amazonaws.com
      
    # 파드 로깅 모니터링
    $ kubectl logs -l app=deploy-websrv -f
      
    # 분산 접속 확인
    $ NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
    $ curl -s $NLB
    # => Hostname: deploy-echo-857b6cfb88-5xz5j
    #    
    #    Pod Information:
    #     -no pod information available-
    #    
    #    Server values:
    #     server_version=nginx: 1.13.0 - lua: 10008
    #    
    #    Request Information:
    #     client_address=192.168.3.171
    #     method=GET
    #     real path=/
    #     query=
    #     request_version=1.1
    #     request_uri=http://k8s-default-svcnlbip-50f5d03f41-f28c6aa861a2e815.elb.ap-northeast-2.amazonaws.com:8080/
    #    
    #    Request Headers:
    #     accept=*/*
    #     host=k8s-default-svcnlbip-50f5d03f41-f28c6aa861a2e815.elb.ap-northeast-2.amazonaws.com
    #     user-agent=curl/8.3.0
    #    
    #    Request Body:
    #     -no body in request-
    $ for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr
    # => 52 Hostname: deploy-echo-857b6cfb88-452cd
    #    48 Hostname: deploy-echo-857b6cfb88-5xz5j
      
    # 지속적인 접속 시도 : 아래 상세 동작 확인 시 유용(패킷 덤프 등)
    $ while true; do curl -s --connect-timeout 1 $NLB | egrep 'Hostname|client_address'; echo "----------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done
    # => Hostname: deploy-echo-857b6cfb88-5xz5j
    #     client_address=192.168.1.39
    #    ----------
    #    2024-10-01 17:52:08
    #    Hostname: deploy-echo-857b6cfb88-452cd
    #     client_address=192.168.3.171
    #    ----------
    #    2024-10-01 17:52:09
    #    Hostname: deploy-echo-857b6cfb88-5xz5j
    #     client_address=192.168.1.39
    #    ----------
    #    2024-10-01 17:52:10
    #    Hostname: deploy-echo-857b6cfb88-452cd
    #     client_address=192.168.3.171
    #    ...
    
  • 파드 2개 → 1개 → 3개 설정 시 동작을 확인해보겠습니다. 파드의 IP가 auto discovery되는데 이것은 service에 엔드포인트 정보를 사용한 것입니다.

    # (신규 터미널) 모니터링
    $ while true; do aws elbv2 describe-target-health --target-group-arn $TARGET_GROUP_ARN --output text; echo; done
    # => TARGETHEALTHDESCRIPTIONS 8080
    #    ADMINISTRATIVEOVERRIDE No override is currently active on target AdministrativeOverride.NoOverride no_override
    #    TARGET ap-northeast-2b 192.168.2.24  8080
    #    TARGETHEALTH healthy
    #    TARGETHEALTHDESCRIPTIONS 8080
    #    ADMINISTRATIVEOVERRIDE No override is currently active on target AdministrativeOverride.NoOverride no_override
    #    TARGET ap-northeast-2a 192.168.1.112 8080
    #    TARGETHEALTH healthy
    #    ...
        
    # 작업용 EC2 - 파드 1개 설정 
    $ kubectl scale deployment deploy-echo --replicas=1
        
    # 확인
    $ kubectl get deploy,pod,svc,ep
    # => NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
    #    deployment.apps/deploy-echo   1/1     1            1           6m34s
    #    
    #    NAME                               READY   STATUS    RESTARTS   AGE
    #    pod/deploy-echo-857b6cfb88-452cd   1/1     Running   0          6m34s
    #    
    #    NAME                      TYPE           CLUSTER-IP      EXTERNAL-IP                                                                         PORT(S)        AGE
    #    service/kubernetes        ClusterIP      10.100.0.1      &lt;none&gt;                                                                              443/TCP        82m
    #    service/svc-nlb-ip-type   LoadBalancer   10.100.83.184   k8s-default-svcnlbip-50f5d03f41-f28c6aa861a2e815.elb.ap-northeast-2.amazonaws.com   80:30771/TCP   6m34s
    #    
    #    NAME                        ENDPOINTS                            AGE
    #    endpoints/kubernetes        192.168.1.65:443,192.168.3.189:443   82m
    #    endpoints/svc-nlb-ip-type   192.168.2.24:8080                    6m34s
    $ curl -s $NLB
    # => Hostname: deploy-echo-857b6cfb88-452cd
    #    
    #    Pod Information:
    #     -no pod information available-
    #    
    #    Server values:
    #     server_version=nginx: 1.13.0 - lua: 10008
    #    
    #    Request Information:
    #     client_address=192.168.3.171
    #     method=GET
    #     real path=/
    #     query=
    #     request_version=1.1
    #     request_uri=http://k8s-default-svcnlbip-50f5d03f41-f28c6aa861a2e815.elb.ap-northeast-2.amazonaws.com:8080/
    #    
    #    Request Headers:
    #     accept=*/*
    #     host=k8s-default-svcnlbip-50f5d03f41-f28c6aa861a2e815.elb.ap-northeast-2.amazonaws.com
    #     user-agent=curl/8.3.0
    #    
    #    Request Body:
    #     -no body in request-
    $ for i in {1..100}; do curl -s --connect-timeout 1 $NLB | grep Hostname ; done | sort | uniq -c | sort -nr
    # =>     100 Hostname: deploy-echo-857b6cfb88-452cd
        
    # 작업용 EC2 - 파드 3개 설정 
    $ kubectl scale deployment deploy-echo --replicas=3
    # => deployment.apps/deploy-echo scaled
        
    # 확인 : NLB 대상 타켓이 아직 initial 일 때 100번 반복 접속 시 어떻게 되는지 확인해보자!
    $ kubectl get deploy,pod,svc,ep
    # => NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
    #    deployment.apps/deploy-echo   3/3     3            3           7m41s
    #    
    #    NAME                               READY   STATUS    RESTARTS   AGE
    #    ...
    #    pod/deploy-echo-857b6cfb88-8wn7b   1/1     Running   0          10s
    #    pod/deploy-echo-857b6cfb88-qhqbt   1/1     Running   0          10s
    #    
    #    ...
    #    
    #    NAME                        ENDPOINTS                                                 AGE
    #    ...
    #    endpoints/svc-nlb-ip-type   192.168.1.161:8080,192.168.2.24:8080,192.168.3.241:8080   7m41s
    $ curl -s $NLB
    $ for i in {1..100}; do curl -s --connect-timeout 1 $NLB | grep Hostname ; done | sort | uniq -c | sort -nr
    # =>      37 Hostname: deploy-echo-857b6cfb88-452cd
    #         33 Hostname: deploy-echo-857b6cfb88-8wn7b
    #         30 Hostname: deploy-echo-857b6cfb88-qhqbt
        
    # 
    $ kubectl describe deploy -n kube-system aws-load-balancer-controller | grep -i 'Service Account'
    # =>   Service Account:  aws-load-balancer-controller
        
    # [AWS LB Ctrl] 클러스터 롤 바인딩 정보 확인
    $ kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
        
    # [AWS LB Ctrl] 클러스터롤 확인 
    $ kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
    
    • 실습 리소스 삭제: kubectl delete deploy deploy-echo; kubectl delete svc svc-nlb-ip-type
  • (심화) Pod readiness gate : ALB/NLB 대상(ip mode)이 ALB/NLB의 헬스체크에 의해 정상일 경우 해당 파드로 전달할 수 있는 기능입니다. - Link K8S
    • 사전 준비

      # 바로 위에서 실습 리소스 삭제했다면, 다시 생성 : deregistration_delay.timeout_seconds=60 확인
      $ kubectl apply -f echo-service-nlb.yaml
      # => deployment.apps/deploy-echo created
      #    service/svc-nlb-ip-type created
      $ kubectl scale deployment deploy-echo --replicas=1
      # => deployment.apps/deploy-echo scaled
          
      #
      $ kubectl get pod -owide
      # => NAME                           READY   STATUS    RESTARTS   AGE   IP              NODE                                               NOMINATED NODE   READINESS GATES
      #    deploy-echo-857b6cfb88-sx8lg   1/1     Running   0          14m   192.168.3.124   ip-192-168-3-235.ap-northeast-2.compute.internal   &lt;none&gt;           &lt;none&gt;
          
      # mutatingwebhookconfigurations 확인 : mutating 대상(네임스페이스에 아래 매칭 시)
      $ kubectl get mutatingwebhookconfigurations
      # => NAME                            WEBHOOKS   AGE
      #    aws-load-balancer-webhook       3          28m
      #    pod-identity-webhook            1          102m
      #    vpc-resource-mutating-webhook   1          102m
      $ kubectl get mutatingwebhookconfigurations aws-load-balancer-webhook -o yaml | kubectl neat
      # => ...
      #      name: mpod.elbv2.k8s.aws
      #      namespaceSelector:
      #        matchExpressions:
      #        - key: elbv2.k8s.aws/pod-readiness-gate-inject
      #          operator: In
      #          values:
      #          - enabled
      #      objectSelector:
      #        matchExpressions:
      #        - key: app.kubernetes.io/name
      #          operator: NotIn
      #          values:
      #          - aws-load-balancer-controller
      #    ...
          
      # 현재 확인
      $ kubectl get ns --show-labels
      # => NAME              STATUS   AGE    LABELS
      #    default           Active   103m   kubernetes.io/metadata.name=default
      #    kube-node-lease   Active   103m   kubernetes.io/metadata.name=kube-node-lease
      #    kube-public       Active   103m   kubernetes.io/metadata.name=kube-public
      #    kube-system       Active   103m   kubernetes.io/metadata.name=kube-system
      
    • 설정 및 확인

      # (터미널 각각 2개) 모니터링
      $ ALB_ARN=$(aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-default-svcnlbip`) == `true`].LoadBalancerArn' | jq -r '.[0]')
      $ TARGET_GROUP_ARN=$(aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN | jq -r '.TargetGroups[0].TargetGroupArn')
          
      $ watch -d kubectl get pod,svc,ep -owide
      $ while true; do aws elbv2 describe-target-health --target-group-arn $TARGET_GROUP_ARN --output text; echo; done
          
      #
      $ kubectl label namespace default elbv2.k8s.aws/pod-readiness-gate-inject=enabled
      # => namespace/default labeled
      $ kubectl get ns --show-labels
      # => NAME              STATUS   AGE    LABELS
      #    default           Active   108m   elbv2.k8s.aws/pod-readiness-gate-inject=enabled,kubernetes.io/metadata.name=default
      #    kube-node-lease   Active   108m   kubernetes.io/metadata.name=kube-node-lease
      #    kube-public       Active   108m   kubernetes.io/metadata.name=kube-public
      #    kube-system       Active   108m   kubernetes.io/metadata.name=kube-system
          
      # READINESS GATES 항목 추가 확인
      $ kubectl describe pod
      $ kubectl get pod -owide
      # => NAME                           READY   STATUS    RESTARTS   AGE   IP              NODE                                               NOMINATED NODE   READINESS GATES
      #    deploy-echo-857b6cfb88-sx8lg   1/1     Running   0          20m   192.168.3.124   ip-192-168-3-235.ap-northeast-2.compute.internal   &lt;none&gt;           &lt;none&gt;
          
      #
      $ kubectl delete pod --all
      # => pod &quot;deploy-echo-857b6cfb88-sx8lg&quot; deleted
      $ kubectl get pod -owide
      # => NAME                           READY   STATUS    RESTARTS   AGE   IP             NODE                                              NOMINATED NODE   READINESS GATES
      #    deploy-echo-857b6cfb88-njwdj   1/1     Running   0          54s   192.168.2.18   ip-192-168-2-92.ap-northeast-2.compute.internal   &lt;none&gt;           1/1
          
      $ kubectl describe pod
      # => ...
      #    Readiness Gates:
      #      Type                                                          Status
      #      target-health.elbv2.k8s.aws/k8s-default-svcnlbip-2e5acb9719   True
      #    Conditions:
      #      Type                                                          Status
      #      target-health.elbv2.k8s.aws/k8s-default-svcnlbip-2e5acb9719   True
      #      PodReadyToStartContainers                                     True
      #      Initialized                                                   True
      #      Ready                                                         True
      #      ContainersReady                                               True
      #      PodScheduled                                                  True
      #      ...
          
      $ kubectl get pod -o yaml | yh
      # => ...
      #        readinessGates:
      #        - conditionType: target-health.elbv2.k8s.aws/k8s-default-svcnlbip-2e5acb9719
      #    ...
      #      status:
      #        conditions:
      #        - lastProbeTime: null
      #          lastTransitionTime: &quot;2024-10-01T09:21:28Z&quot;
      #          status: &quot;True&quot;
      #          type: target-health.elbv2.k8s.aws/k8s-default-svcnlbip-2e5acb9719
      #    ...
          
      # 분산 접속 확인
      $ NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
      $ curl -s $NLB
      # => Hostname: deploy-echo-857b6cfb88-njwdj
      #    
      #    Pod Information:
      #     -no pod information available-
      #    
      #    Server values:
      #     server_version=nginx: 1.13.0 - lua: 10008
      #    
      #    Request Information:
      #     client_address=192.168.3.71
      #     method=GET
      #     real path=/
      #     query=
      #     request_version=1.1
      #     request_uri=http://k8s-default-svcnlbip-f4c21d9697-0d3512de2bf74357.elb.ap-northeast-2.amazonaws.com:8080/
      #    
      #    Request Headers:
      #     accept=*/*
      #     host=k8s-default-svcnlbip-f4c21d9697-0d3512de2bf74357.elb.ap-northeast-2.amazonaws.com
      #     user-agent=curl/8.3.0
      #    
      #    Request Body:
      #     -no body in request-
      $ for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr
      # =>     100 Hostname: deploy-echo-857b6cfb88-njwdj
      
    • 실습 리소스 삭제: kubectl delete deploy deploy-echo; kubectl delete svc svc-nlb-ip-type

  • NLB 대상 타켓을 Instance mode 로 설정해보기
  • NLB IP Target & Proxy Protocol v2 활성화 : NLB에서 바로 파드로 인입 및 ClientIP 확인 설정 - 링크 image 참고

    img_9.png

# 생성
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gasida-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gasida-web
  template:
    metadata:
      labels:
        app: gasida-web
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: gasida-web
        image: sweetlittlebird/httpd:pp
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-ip-type-pp
  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-proxy-protocol: "*"
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    app: gasida-web
EOF
# => deployment.apps/gasida-web created
#    service/svc-nlb-ip-type-pp created

# apache에 proxy protocol 활성화 확인
$ kubectl exec deploy/gasida-web -- apachectl -t -D DUMP_MODULES
# => Loaded Modules:
#     ...
#     remoteip_module (shared)
#     ...
$ kubectl exec deploy/gasida-web -- cat /usr/local/apache2/conf/httpd.conf
# => ...
#    RemoteIPProxyProtocol On
#    ...

# 확인
$ kubectl get svc,ep
# => NAME                         TYPE           CLUSTER-IP       EXTERNAL-IP                                                                         PORT(S)        AGE
#    service/kubernetes           ClusterIP      10.100.0.1       &lt;none&gt;                                                                              443/TCP        5h52m
#    service/svc-nlb-ip-type-pp   LoadBalancer   10.100.160.172   k8s-default-svcnlbip-c11e4bd02e-ec82d8f688f176d3.elb.ap-northeast-2.amazonaws.com   80:31348/TCP   36m
#    
#    NAME                           ENDPOINTS                            AGE
#    endpoints/kubernetes           192.168.1.65:443,192.168.3.189:443   5h52m
#    endpoints/svc-nlb-ip-type-pp   192.168.2.51:80                      36m
$ kubectl describe svc svc-nlb-ip-type-pp
$ kubectl describe svc svc-nlb-ip-type-pp | grep Annotations: -A5
# => Annotations: service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: true
#                 service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip   # <span style="color: green;">👉 NLB Target Type이 IP입니다.</span>
#                 service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: *
#                 service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
#    Selector:    app=gasida-web
#    Type:        LoadBalancer

# 접속 확인
$ NLB=$(kubectl get svc svc-nlb-ip-type-pp -o jsonpath={.status.loadBalancer.ingress[0].hostname})
$ curl -s $NLB
# => &lt;html&gt;&lt;body&gt;&lt;h1&gt;It works!&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;

# 지속적인 접속 시도 : 아래 상세 동작 확인 시 유용(패킷 덤프 등)
$ while true; do curl -s --connect-timeout 1 $NLB; echo "----------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done

# 베스쳔 호스트 IP 확인
$ curl ipinfo.io/ip
# => 3.35.140.75

# 로그 확인
$ kubectl logs -l app=gasida-web -f
# => ...
#    3.35.140.75 - - [01/Oct/2024:13:46:35 +0000] &quot;GET / HTTP/1.1&quot; 200 45
#    3.35.140.75 - - [01/Oct/2024:13:46:36 +0000] &quot;GET / HTTP/1.1&quot; 200 45
#    3.35.140.75 - - [01/Oct/2024:13:46:37 +0000] &quot;GET / HTTP/1.1&quot; 200 45
#    3.35.140.75 - - [01/Oct/2024:13:46:38 +0000] &quot;GET / HTTP/1.1&quot; 200 45

# <span style="color: green;">👉 proxy protocol을 통해 외부 클라이언트의 IP가 전달되어 로그에 남는것을 확인할 수 있었습니다.</span>

# 삭제
$ kubectl delete deploy gasida-web; kubectl delete svc svc-nlb-ip-type-pp

Ingress

  • Ingress는 클러스터 내부의 서비스(ClusterIP, NodePort, Loadbalancer)를 외부로 노출(HTTP/HTTPS)시키는 일종의 Web Proxy 역할을 수행합니다.

AWS Load Balancer Controller + Ingress (ALB) IP 모드 동작 with AWS VPC CNI

img.png

  • 서비스/파드 배포 테스트 with Ingress(ALB) - ALB

    # 게임 파드와 Service, Ingress 배포
    $ curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ingress1.yaml
    $ cat ingress1.yaml
    $ kubectl apply -f ingress1.yaml
    # => namespace/game-2048 created
    #    deployment.apps/deployment-2048 created
    #    service/service-2048 created
    #    ingress.networking.k8s.io/ingress-2048 created
      
    # 모니터링
    $ watch -d kubectl get pod,ingress,svc,ep -n game-2048
      
    # 생성 확인
    $ kubectl get-all -n game-2048
    # => NAME                                                               NAMESPACE  AGE
    #    configmap/kube-root-ca.crt                                         game-2048  38s
    #    endpoints/service-2048                                             game-2048  38s
    #    pod/deployment-2048-85f8c7d69-gz295                                game-2048  38s
    #    pod/deployment-2048-85f8c7d69-t2blb                                game-2048  38s
    #    serviceaccount/default                                             game-2048  38s
    #    service/service-2048                                               game-2048  38s
    #    deployment.apps/deployment-2048                                    game-2048  38s
    #    replicaset.apps/deployment-2048-85f8c7d69                          game-2048  38s
    #    endpointslice.discovery.k8s.io/service-2048-s58q4                  game-2048  38s
    #    targetgroupbinding.elbv2.k8s.aws/k8s-game2048-service2-00c3b27023  game-2048  34s
    #    ingress.networking.k8s.io/ingress-2048                             game-2048  38s
    $ kubectl get ingress,svc,ep,pod -n game-2048
    # => NAME                                     CLASS   HOSTS   ADDRESS                                                                       PORTS   AGE
    #    ingress.networking.k8s.io/ingress-2048   alb     *       k8s-game2048-ingress2-70d50ce3fd-333540411.ap-northeast-2.elb.amazonaws.com   80      49s
    #    
    #    NAME                   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
    #    service/service-2048   NodePort   10.100.41.151   &lt;none&gt;        80:32522/TCP   49s
    #    
    #    NAME                     ENDPOINTS                          AGE
    #    endpoints/service-2048   192.168.1.112:80,192.168.2.18:80   49s
    #    
    #    NAME                                  READY   STATUS    RESTARTS   AGE
    #    pod/deployment-2048-85f8c7d69-gz295   1/1     Running   0          49s
    #    pod/deployment-2048-85f8c7d69-t2blb   1/1     Running   0          49s
    $ kubectl get targetgroupbindings -n game-2048
    # => NAME                               SERVICE-NAME   SERVICE-PORT   TARGET-TYPE   AGE
    #    k8s-game2048-service2-00c3b27023   service-2048   80             ip            56s
      
    # 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
    # => {
    #      &quot;TargetHealthDescriptions&quot;: [
    #        {
    #          &quot;Target&quot;: {
    #            &quot;Id&quot;: &quot;192.168.1.112&quot;,
    #            &quot;Port&quot;: 80,
    #            &quot;AvailabilityZone&quot;: &quot;ap-northeast-2a&quot;
    #          },
    #          &quot;HealthCheckPort&quot;: &quot;80&quot;,
    #          &quot;TargetHealth&quot;: {
    #            &quot;State&quot;: &quot;healthy&quot;
    #          }
    #        },
    #        {
    #          &quot;Target&quot;: {
    #            &quot;Id&quot;: &quot;192.168.2.18&quot;,
    #            &quot;Port&quot;: 80,
    #            &quot;AvailabilityZone&quot;: &quot;ap-northeast-2b&quot;
    #          },
    #          &quot;HealthCheckPort&quot;: &quot;80&quot;,
    #          &quot;TargetHealth&quot;: {
    #            &quot;State&quot;: &quot;healthy&quot;
    #          }
    #        }
    #      ]
    #    }
      
    # Ingress 확인
    $ kubectl describe ingress -n game-2048 ingress-2048
    $ kubectl get ingress -n game-2048 ingress-2048 -o jsonpath="{.status.loadBalancer.ingress[*].hostname}{'\n'}"
    # => k8s-game2048-ingress2-70d50ce3fd-333540411.ap-northeast-2.elb.amazonaws.com
      
    # 게임 접속 : ALB 주소로 웹 접속
    $ kubectl get ingress -n game-2048 ingress-2048 -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Game URL = http://"$1 }'
    # => Game URL = http://k8s-game2048-ingress2-70d50ce3fd-333540411.ap-northeast-2.elb.amazonaws.com
      
    # 파드 IP 확인
    $ kubectl get pod -n game-2048 -owide
    # => NAME                              READY   STATUS    RESTARTS   AGE     IP              NODE                                               NOMINATED NODE   READINESS GATES
    #    deployment-2048-85f8c7d69-gz295   1/1     Running   0          3m42s   192.168.2.18    ip-192-168-2-92.ap-northeast-2.compute.internal    &lt;none&gt;           &lt;none&gt;
    #    deployment-2048-85f8c7d69-t2blb   1/1     Running   0          3m42s   192.168.1.112   ip-192-168-1-186.ap-northeast-2.compute.internal   &lt;none&gt;           &lt;none&gt;
    

    img.png

    • ALB 대상 그룹에 등록된 대상 확인 : ALB에서 파드 IP로 직접 전달

      img.png 파드 IP로 바로 직접 연결된 ALB 대상 그룹

    • 파드 3개로 증가

      # 터미널1
      $ watch kubectl get pod -n game-2048
      $ while true; do aws elbv2 describe-target-health --target-group-arn $TARGET_GROUP_ARN --output text; echo; done
          
      # 터미널2 : 파드 3개로 증가
      $ kubectl scale deployment -n game-2048 deployment-2048 --replicas 3
      # => deployment.apps/deployment-2048 scaled
      

      img.png 추가된 파드 IP가 연결됨

    • 파드 1개로 감소

      # 터미널2 : 파드 1개로 감소
      $ kubectl scale deployment -n game-2048 deployment-2048 --replicas 1
      # => deployment.apps/deployment-2048 scaled
      

      img.png 삭제되는 2개의 파드가 삭제 중임을 확인

    • 실습 리소스 삭제

      $ kubectl delete ingress ingress-2048 -n game-2048
      $ kubectl delete svc service-2048 -n game-2048 && kubectl delete deploy deployment-2048 -n game-2048 && kubectl delete ns game-2048
      

Kubernetes 응용프로그램을 외부로 노출 시키는 방법들 비교

  • 간략하게 Kubernetes 응용프로그램을 외부로 노출 시기는 방법들을 비교해보겠습니다.
  • 자세한 내용은 다음 블로그에서 살펴 볼 수 있습니다. Exposing Kubernetes Applications, Part 1: Service and Ingress Resources
    1. Exposing a Service : In-tree Service Controller img.png
      • AWS CLB(Classic Load Balancer)나 AWS NLB(Network Load Balancer)를 사용하여 서비스를 직접 외부로 노출합니다. 하지만 서비스 수가 많아지면 Load Balancer의 수도 많아지게 되어 관리가 어려워집니다.
    2. Ingress Implementations : External Load Balancer img.png
      • 외부에 ALB(Application Load Balancer)를 생성하고 라우팅합니다. 이때 외부의 ALB가 Ingress rule을 ALB rule로 변환하여 외부 ALB가 직접 파드와 통신합니다. 앞에서 본 서비스를 직접 외부로 노출하는것 보다, CSP(Cloud Service Provider)에서 제공하는 확장성이나, DDOS 방어기능 등을 활용 할 수 있습니다.
    3. Ingress Implementations : Internal Reverse Proxy img.png
      • 외부의 ALB에 더해 nginx 등의 Layer 7 리버스 프록시를 두는 방식입니다. 이 방식은 외부 ALB가 직접 파드와 통신하는 것이 아니라, 리버스 프록시를 통해 통신하게 됩니다. 성능상으로는 불이익이 있지만 L7 리버스 프록시에서 제공하는 추가 기능들을 활용 할 수 있습니다.
      • 하지만 관리요소가 추가되는 것이기 때문에 이를 고려하여 사용해야 합니다.
    4. Kubernetes Gateway API img.png
      • Kubernetes Gateway API는 Ingress Controller를 대체하는 새로운 API로 현시점에서 Beta 상태로, 정식 지원하지는 않는듯하며, 외부 Loadbalancer나 Internal reverse proxy 방식처럼 사용할 수 있습니다.

ExternalDNS

  • K8S 서비스/인그레스 생성 시 도메인을 설정하면, AWS(Route 53), Azure(DNS), GCP(Cloud DNS)에 A 레코드(TXT 레코드)가 자동으로 생성/삭제됩니다. img.png https://edgehog.blog/a-self-hosted-external-dns-resolver-for-kubernetes-111a27d6fc2c

  • AWS Route 53 정보 확인 & 변수 지정 : Public 도메인 소유를 하고 있어야 합니다

    # 자신의 도메인 변수 지정 : 소유하고 있는 자신의 도메인을 입력하시면 됩니다
    #$ MyDomain=<자신의 도메인>
    $ MyDomain=kans.loremipsum.sweetlittlebird.io
    $ echo "export MyDomain=kans.loremipsum.sweetlittlebird.io" >> /etc/profile
      
    # 자신의 Route 53 도메인 ID 조회 및 변수 지정
    $ aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." | jq
    # => {
    #      &quot;HostedZones&quot;: [
    #        {
    #          &quot;Id&quot;: &quot;/hostedzone/Z0416620XQJAGAPWXO31&quot;,
    #          &quot;Name&quot;: &quot;kans.loremipsum.sweetlittlebird.io.&quot;,
    #          &quot;CallerReference&quot;: &quot;3aba3324-d6a0-44a4-82b5-004044e06dd6&quot;,
    #          &quot;Config&quot;: {
    #            &quot;Comment&quot;: &quot;&quot;,
    #            &quot;PrivateZone&quot;: false
    #          },
    #          &quot;ResourceRecordSetCount&quot;: 3
    #        }
    #      ],
    #      &quot;DNSName&quot;: &quot;kans.loremipsum.sweetlittlebird.io.&quot;,
    #      &quot;IsTruncated&quot;: false,
    #      &quot;MaxItems&quot;: &quot;100&quot;
    #    }
    $ aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Name"
    # => &quot;kans.loremipsum.sweetlittlebird.io.&quot;
    $ aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text
    # => /hostedzone/Z0416620XQJAGAPWXO31
    $ MyDnzHostedZoneId=`aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text`
    $ echo $MyDnzHostedZoneId
    # => /hostedzone/Z0416620XQJAGAPWXO31
      
    # (옵션) NS 레코드 타입 첫번째 조회
    $ aws route53 list-resource-record-sets --output json --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'NS']" | jq -r '.[0].ResourceRecords[].Value'
    # => ns-979.awsdns-58.net.
    #    ns-1489.awsdns-58.org.
    #    ns-355.awsdns-44.com.
    #    ns-1760.awsdns-28.co.uk.
    # (옵션) A 레코드 타입 모두 조회
    $ aws route53 list-resource-record-sets --output json --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']"
    # => []
      
    # A 레코드 타입 조회
    $ aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq
    $ aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" | jq
    $ aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" --output text
      
    # A 레코드 값 반복 조회
    $ while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done
    
  • ExternalDNS 설치 - 링크
    # EKS 배포 시 Node IAM Role 설정되어 있음
    # eksctl create cluster ... --external-dns-access ...
      
    # 
    #$ MyDomain=<자신의 도메인>
    $ MyDomain=kans.loremipsum.sweetlittlebird.io
      
    # 자신의 Route 53 도메인 ID 조회 및 변수 지정
    $ MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
      
    # 변수 확인
    $ echo $MyDomain, $MyDnzHostedZoneId
    # => kans.loremipsum.sweetlittlebird.io, /hostedzone/Z0416620XQJAGAPWXO31
      
    # ExternalDNS 배포
    $ 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 -
    # => 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
      
    # 확인 및 로그 모니터링
    $ kubectl get pod -l app.kubernetes.io/name=external-dns -n kube-system
    # => NAME                            READY   STATUS    RESTARTS   AGE
    #    external-dns-648996678b-7r4mz   1/1     Running   0          12s
    $ kubectl logs deploy/external-dns -n kube-system -f
    
    • (참고) 기존에 ExternalDNS를 통해 사용한 A/TXT 레코드가 있는 존의 경우에 policy 정책을 upsert-only 로 설정 후 사용 하자 - Link

      - #--policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
      
  • Service(NLB) + 도메인 연동(ExternalDNS) - 도메인체크
    # 터미널1 (모니터링)
    $ watch -d 'kubectl get pod,svc'
    $ kubectl logs deploy/external-dns -n kube-system -f
    # => ...
    #    time=&quot;2024-10-01T14:18:10Z&quot; level=info msg=&quot;Instantiating new Kubernetes client&quot;
    #    time=&quot;2024-10-01T14:18:10Z&quot; level=info msg=&quot;Using inCluster-config based on serviceaccount-token&quot;
    #    time=&quot;2024-10-01T14:18:10Z&quot; level=info msg=&quot;Created Kubernetes client https://10.100.0.1:443&quot;
    #    time=&quot;2024-10-01T14:18:11Z&quot; level=info msg=&quot;Applying provider record filter for domains: [kans.loremipsum.sweetlittlebird.io. .kans.loremipsum.sweetlittlebird.io.]&quot;
    #    time=&quot;2024-10-01T14:18:11Z&quot; level=info msg=&quot;All records are already up to date&quot;
    #    ... (deployment 배포 후) ...
    #    time=&quot;2024-10-01T14:20:11Z&quot; level=info msg=&quot;Applying provider record filter for domains: [kans.loremipsum.sweetlittlebird.io. .kans.loremipsum.sweetlittlebird.io.]&quot;
    #    time=&quot;2024-10-01T14:20:12Z&quot; level=info msg=&quot;Desired change: CREATE cname-tetris.kans.loremipsum.sweetlittlebird.io TXT&quot; profile=default zoneID=/hostedzone/Z0416620XQJAGAPWXO31 zoneName=kans.loremipsum.sweetlittlebird.io.
    #    time=&quot;2024-10-01T14:20:12Z&quot; level=info msg=&quot;Desired change: CREATE tetris.kans.loremipsum.sweetlittlebird.io A&quot; profile=default zoneID=/hostedzone/Z0416620XQJAGAPWXO31 zoneName=kans.loremipsum.sweetlittlebird.io.
    #    time=&quot;2024-10-01T14:20:12Z&quot; level=info msg=&quot;Desired change: CREATE tetris.kans.loremipsum.sweetlittlebird.io TXT&quot; profile=default zoneID=/hostedzone/Z0416620XQJAGAPWXO31 zoneName=kans.loremipsum.sweetlittlebird.io.
    #    time=&quot;2024-10-01T14:20:12Z&quot; level=info msg=&quot;3 record(s) were successfully updated&quot; profile=default zoneID=/hostedzone/Z0416620XQJAGAPWXO31 zoneName=kans.loremipsum.sweetlittlebird.io.
      
    # 테트리스 디플로이먼트 배포
    $ 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
      
    # 배포 확인
    $ kubectl get deploy,svc,ep tetris
    # => NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
    #    deployment.apps/tetris   0/1     1            0           5s
    #    
    #    NAME             TYPE           CLUSTER-IP      EXTERNAL-IP                                                                       PORT(S)        AGE
    #    service/tetris   LoadBalancer   10.100.39.204   k8s-default-tetris-4d2a39458c-e91cd840ce214644.elb.ap-northeast-2.amazonaws.com   80:32310/TCP   5s
    #    
    #    NAME               ENDPOINTS   AGE
    #    endpoints/tetris   &lt;none&gt;      5s
      
    # NLB에 ExternanDNS 로 도메인 연결
    $ kubectl annotate service tetris "external-dns.alpha.kubernetes.io/hostname=tetris.$MyDomain"
    # => service/tetris annotated
    $ while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done
    # => [
    #      {
    #        &quot;Name&quot;: &quot;tetris.kans.loremipsum.sweetlittlebird.io.&quot;,
    #        &quot;Type&quot;: &quot;A&quot;,
    #        &quot;AliasTarget&quot;: {
    #          &quot;HostedZoneId&quot;: &quot;ZIBE1TIR4HY56&quot;,
    #          &quot;DNSName&quot;: &quot;k8s-default-tetris-4d2a39458c-e91cd840ce214644.elb.ap-northeast-2.amazonaws.com.&quot;,
    #          &quot;EvaluateTargetHealth&quot;: true
    #        }
    #      }
    #    ]
    #    Sat Oct  1 23:21:27 KST 2024
    #    ...
      
    # Route53에 A레코드 확인
    $ aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq
    # => [
    #      {
    #        &quot;Name&quot;: &quot;tetris.kans.loremipsum.sweetlittlebird.io.&quot;,
    #        &quot;Type&quot;: &quot;A&quot;,
    #        &quot;AliasTarget&quot;: {
    #          &quot;HostedZoneId&quot;: &quot;ZIBE1TIR4HY56&quot;,
    #          &quot;DNSName&quot;: &quot;k8s-default-tetris-4d2a39458c-e91cd840ce214644.elb.ap-northeast-2.amazonaws.com.&quot;,
    #          &quot;EvaluateTargetHealth&quot;: true
    #        }
    #      }
    #    ]
    $ aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" | jq .[]
    # => &quot;tetris.kans.loremipsum.sweetlittlebird.io.&quot;
      
    # 확인
    $ dig +short tetris.$MyDomain @8.8.8.8
    # => 3.38.82.70
    #    3.36.187.152
    #    3.35.184.161
    $ dig +short tetris.$MyDomain
    # => 3.38.82.70
      
    # 도메인 체크
    $ echo -e "My Domain Checker = https://www.whatsmydns.net/#A/tetris.$MyDomain"
    # => My Domain Checker = https://www.whatsmydns.net/#A/tetris.kans.loremipsum.sweetlittlebird.io
      
    # 웹 접속 주소 확인 및 접속
    $ echo -e "Tetris Game URL = http://tetris.$MyDomain"
    # => Tetris Game URL = http://tetris.kans.loremipsum.sweetlittlebird.io
    
    • 웹 접속(http) → 화살표키, 일시중지(space bar)

      img.png

  • Ingress(ALB + HTTPS) + 도메인 연동(ExternalDNS)

    # 테트리스 디플로이먼트 배포
    $ 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
      labels:
        app: tetris
    spec:
      selector:
        app: tetris
      ports:
      - port: 80
        protocol: TCP
        targetPort: 80
      type: LoadBalancer
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: tetris-ingress
      annotations:
        kubernetes.io/ingress.class: alb
        alb.ingress.kubernetes.io/scheme: internet-facing
        alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-2:123456789012:certificate/256538f4-6c5d-4109-9d4d-9aa6af41adaa
        alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
        alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
    spec:
      ingressClassName: alb
      rules:
        - http:
            paths:
            - path: /
              pathType: Prefix
              backend:
                service:
                  name: tetris
                  port:
                    number: 80  
    EOF
    # => deployment.apps/tetris created
    #    service/tetris created
    #    ingress.networking.k8s.io/tetris-ingress created
      
    # ingress에 도메인 부여
    $ kubectl annotate ingress tetris-ingress "external-dns.alpha.kubernetes.io/hostname=tetris2.$MyDomain"
    # => ingress.networking.k8s.io/tetris-ingress annotated
      
    # <span style="color: green;">👉 기존에 사용했던 도메인(tetris.$MyDomain)을 사용하면 DNS 레코드가 전파되는데 시간이 더 걸리기 때문에</span>
    # <span style="color: green;">   다른 도메인 (tetris2.$MyDomain)을 사용하였습니다.</span>
    

    img.png dns 적용 확인 (확인 사이트 링크)

    • https://tetris2.kans.loremipsum.sweetlittlebird.io 로 접속하여 https 통신 확인 img.png https 적용 확인
  • 리소스 삭제 : kubectl delete deploy,svc tetris ← 삭제 시 externaldns 에 의해서 A레코드도 같이 삭제됩니다.
  • (참고) ACM 퍼블릭 인증서 요청 및 해당 인증서에 대한 Route53 도메인 검증 설정 with AWS CLI

    # 각자 자신의 도메인 변수 지정
    #$ MyDomain=<각자 자신의 도메인>
    $ MyDomain=kans.loremipsum.sweetlittlebird.io
      
    # ACM 퍼블릭 인증서 요청
    $ CERT_ARN=$(aws acm request-certificate \
      --domain-name $MyDomain \
      --validation-method 'DNS' \
      --key-algorithm 'RSA_2048' \
      | jq --raw-output '.CertificateArn')
      
    # 생성한 인증서 CNAME 이름 가져오기
    $ CnameName=$(aws acm describe-certificate \
      --certificate-arn $CERT_ARN \
      --query 'Certificate.DomainValidationOptions[*].ResourceRecord.Name' \
      --output text)
      
    # 생성한 인증서 CNAME 값 가져오기
    $ CnameValue=$(aws acm describe-certificate \
      --certificate-arn $CERT_ARN \
      --query 'Certificate.DomainValidationOptions[*].ResourceRecord.Value' \
      --output text)
      
    # 정상 출력 확인하기
    $ echo $CERT_ARN, $CnameName, $CnameValue
    # => arn:aws:acm:ap-northeast-2:123456789012:certificate/256538f4-6c5d-4109-9d4d-9aa6af41adaa, _e784949706a5e6eee7f350436cc9d501.kans.loremipsum.sweetlittlebird.io., _e62ee6e59a48ea2f3d1ab952d289bcea.djqtsrsxkq.acm-validations.aws.
      
    # 레코드 파일
    $ cat <<EOT > cname.json
    {
      "Comment": "create a acm's CNAME record",
      "Changes": [
        {
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "CnameName",
            "Type": "CNAME",
            "TTL": 300,
            "ResourceRecords": [
              {
                "Value": "CnameValue"
              }
            ]
          }
        }
      ]
    }
    EOT
      
    # CNAME 이름, 값 치환하기
    $ sed -i "s/CnameName/$CnameName/g" cname.json
    $ sed -i "s/CnameValue/$CnameValue/g" cname.json
    $ cat cname.json
    # => {
    #      &quot;Comment&quot;: &quot;create a acm's CNAME record&quot;,
    #      &quot;Changes&quot;: [
    #        {
    #          &quot;Action&quot;: &quot;CREATE&quot;,
    #          &quot;ResourceRecordSet&quot;: {
    #            &quot;Name&quot;: &quot;_e784949706a5e6eee7f350436cc9d501.kans.loremipsum.sweetlittlebird.io.&quot;,
    #            &quot;Type&quot;: &quot;CNAME&quot;,
    #            &quot;TTL&quot;: 300,
    #            &quot;ResourceRecords&quot;: [
    #              {
    #                &quot;Value&quot;: &quot;_e62ee6e59a48ea2f3d1ab952d289bcea.djqtsrsxkq.acm-validations.aws.&quot;
    #              }
    #            ]
    #          }
    #        }
    #      ]
    #    }
      
    # 해당 인증서에 대한 Route53 도메인 검증 설정을 위한 Route53 레코드 생성
    $ aws route53 change-resource-record-sets --hosted-zone-id $MyDnzHostedZoneId --change-batch file://cname.json
    

실습 완료 후 자원 삭제

  • 삭제 : 장점(1줄 명령어로 완전 삭제), 단점(삭제 실행과 완료까지 SSH 세션 유지 필요)
    $ eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME
    

마치며

와.. 드디어 마지막 주차인 9주차 과제를 완료하였습니다. 마지막 주차인 만큼 과제를 빨리 마치고 싶었지만, 생각보다 시간이 많이 걸려서 결국 오늘도 자정을 넘긴 일요일입니다. :cry: 매과제마다 저도 시간을 많이 쏟지만, 가시다 님을 비롯하여 스터디 조력자분들이 이 스터디를 위해 지금까지 얼마나 많은 시간을 투자하셨을지 생각하면 스스로가 머쓱하면서도, 감사함과 존경심을 느낍니다.

이번 스터디를 통해 많은 것을 배웠고, 블로그를 작성하는데 있어서도 많은 도움이 되었습니다. 회사에서 필요한 글만 쓰다가, 정말 오랜만에 개인을 위한 글도 이렇게 꾸준히 쓴것도 참 오랜만입니다. 지난 테라폼 스터디부터 하면 5개월 정도는 거의 매주 글을 썼던 것 같습니다. —물론 실습 위주여서 “글”을 쓴게 맞냐는 문제가 있긴합니다.— 예전에는 정말 시덥잖은 블로그 글도 많이 썼었는데, 나이도 들고, 사회적 지위도 (아직은 낮지만 예전보다) 높아지다보니 글을 쓰는데 있어서도 조금은 부담이 되었었는데, 이번 스터디를 통해 다시 글을 쓰는 재미와 글을 쓸 수 있다는 자신감을 느끼게 되었습니다. 앞으로도 스터디가 아니더라도 꾸준히 글을 쓰고 싶습니다.

약 3개월 간 스터디를 잘 이끌어주신 가시다 님과 조력자 분들께 큰 감사를 드립니다. :bow:

스터디에 참여하신 모든 분들도 고생 많으셨습니다. 다음에 또 뵐 수 있기를 기대합니다. :smile: