쿠버네티스 네트워크
쿠버네티스에서는 네트워크 환경을 구성하여 파드 간 통신을 위해 CNI(Container Network Interface)를 사용합니다. 쿠버네티스에서 기본적으로 제공하는 CNI(kubenet)도 있으나, 기능 제한사항이 많아 이를 보완하기 위해 보통 3rd-party 플러그인을 사용합니다. 대표적으로 Calico, Flannel 등이 있습니다.
실습의 경우 AWS 환경에서 진행하므로 AWS에서 제공하는 CNI인 AWS VPC CNI를 사용합니다.
타 CNI와 비교했을 때, AWS VPC CNI의 가장 큰 특징은 파드가 존재하는 워커 노드의 IP 네트워크 대역과 파드의 IP 대역이 같아 직접 통신이 가능하다는 점입니다. 또한 AWS VPC와 통합되어 VPC FlowLog, 라우팅 정책, 보안그룹 사용이 가능합니다.
kops 노드의 기본 네트워크 정보 확인
미리 구축한 kops 실습 환경 중 마스터 노드와 워커 노드의 기본 네트워크 정보를 확인해보겠습니다.
# 노드 IP 확인
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 |
+---------------------------------------------------+----------------+-----------------+-----------+
| nodes-ap-northeast-2c.jjikin.com | 172.30.83.210 | 3.35.3.157 | running |
| kops-ec2 | 10.0.0.10 | 52.78.213.159 | running |
| nodes-ap-northeast-2a.jjikin.com | 172.30.57.0 | 43.201.55.187 | running |
| control-plane-ap-northeast-2a.masters.jjikin.com | 172.30.45.82 | 13.124.187.229 | running |
+---------------------------------------------------+----------------+-----------------+-----------+
# 마스터 노드 SSH 접속 확인
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME
sudo apt install -y tree jq net-tools
# 워커 노드 접속 편의를 위한 설정
W1PIP=43.201.55.187
W2PIP=3.35.3.157
# 워커 노드 SSH 접속 확인
ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP, $W2PIP
sudo apt install -y tree jq net-tools
# 네트워크 정보 확인 : eniY는 pod network 네임스페이스와 veth pair
ip -br -c addr
ip -c addr
ip -c route
sudo iptables -t nat -S
sudo iptables -t nat -L -n -v
Bash
추후 CNI 네트워크 관련 에러 발생 시 아래 로그를 통해 확인 가능합니다.
# 마스터 노드에서 AWS CNI Log 확인
ubuntu@i-06ea5b80f74f8be78:~$ ls /var/log/aws-routed-eni
egress-v4-plugin.log ipamd.log plugin.log
cat /var/log/aws-routed-eni/ipamd.log | jq
{
"level": "debug",
"ts": "2023-03-18T07:13:18.874Z",
"caller": "ipamd/ipamd.go:1246",
"msg": "ENI eni-0d6ab9d3ac1bbaf53 cannot be deleted because it is primary"
}
...
Bash
다음은 AWS VPC CNI의 특징인 실제 워커 노드의 IP(ENI IP)와 파드의 IP 대역이 같은지 확인해보겠습니다.
노드 내에서 Network Namespace는 호스트(Root)와 파드 별(Per Pod)로 구분됩니다.
파드 중에서는 마스터 노드에 연결된 ENI IP(172.30.45.82)를 그대로 사용하는 파드가 존재하는데, kube-system 네임스페이스에 존재하는 파드가 이에 해당합니다.
그외 파드들은 마스터 노드의 ENI IP 대역과 같은 대역을 공유함을 알 수 있습니다.
실제로 테스트용 파드를 생성해보면서 IP 부여가 되는지 확인해보겠습니다.
ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP
watch -d "ip link | egrep 'ens5|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP
watch -d "ip link | egrep 'ens5|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
# 테스트용 파드 netshoot-pod 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: netshoot-pod
spec:
replicas: 2
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
Bash
[파드 생성 전]
Worker#1, #2
[파드 생성 후]
노드 간 파드의 통신
AWS VPC CNI를 사용했을 때, 파드 간 통신 플로우는 아래와 같습니다.
앞서 언급한 것처럼 NAT와 같은 별도의 IP 변경 없이 VPC Native하게 파드 간 직접 통신이 가능합니다.
tcpdump 툴을 이용하여 파드 간 통신을 직접 확인해보겠습니다.
# 파드명, IP를 변수로 지정
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})
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})
# 각 워커 노드에서 tcpdump 실행
sudo tcpdump -i any -nn icmp
# 파드 간 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP1
# ping은 파드 내 default rule을 통해 라우팅된다.
ubuntu@i-04cd4633ea499b0c4:~$ ip route show table main
default via 172.30.64.1 dev ens5 proto dhcp src 172.30.83.210 metric 100
Bash
파드에서 외부로의 통신
AWS VPC CNI를 사용했을 때, 파드에서 외부로의 통신 플로우는 아래와 같습니다.
파드의 IP에서 파드가 속한 워커 노드의 ENI Public IP로 SNAT되어 통신하게 됩니다.
tcpdump, curl 툴을 이용하여 외부 통신을 직접 확인해보겠습니다.
# 각 워커 노드에서 tcpdump 실행
sudo tcpdump -i any -nn icmp
# ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 1 www.google.com
kubectl exec -it $PODNAME2 -- ping -c 1 www.google.com
# ping은 파드 내 SNAT rule을 통해 라우팅된다.
ubuntu@i-07ba5de9d32b053f1:~$ sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 ! -d 172.30.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j AWS-SNAT-CHAIN-1
-A AWS-SNAT-CHAIN-1 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 172.30.57.0 --random-fully
# 테스트용 파드 삭제
kubectl delete deploy netshoot-pod
Bash
www.google.com로 ping 테스트 시 172.30 대역에서 216.58.220.132(google)로 전송되었으며,
파드에서 curl을 통해 ip 확인 시 워커노드 1번의 ENI Public IP(43.201.55.187)을 확인할 수 있었습니다.
노드 내 파드 생성 갯수 제한
노드에 생성할 수 있는 파드 갯수에는 한계가 있으며, 이는 인스턴스의 타입 별 최대 ENI 갯수와 할당 가능한 최대 IP 갯수에 따라 결정됩니다.
호스트 IP를 사용하는 aws-node, kube-proxy 파드는 제외이며 최대 갯수를 구하는 공식은 아래와 같습니다.
최대 파드 생성 갯수
( 인스턴스의 타입 별 최대 ENI 갯수 * ( ENI에 할당 가능한 최대 IP 갯수 - 1 ) ) + 2
# 현재 사용 중인 워커 노드의 타입(c5d.large)의 정보(필터) 확인
aws ec2 describe-instance-types --filters Name=instance-type,Values=c5d.* \
--query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
--output table
----------------------------------------
| DescribeInstanceTypes |
+----------+----------+----------------+
| IPv4addr | MaxENI | Type |
+----------+----------+----------------+
| 10 | 3 | c5d.large |
| 30 | 8 | c5d.12xlarge |
| 50 | 15 | c5d.18xlarge |
| 15 | 4 | c5d.2xlarge |
| 50 | 15 | c5d.24xlarge |
| 30 | 8 | c5d.4xlarge |
| 15 | 4 | c5d.xlarge |
| 50 | 15 | c5d.metal |
| 30 | 8 | c5d.9xlarge |
+----------+----------+----------------+
-> c5d.large 에서는 최대 29개 파드까지 생성 가능합니다. (3*(10-1))+2
Bash
이 이상 생성이 필요한 경우,
LimitRange 설정, Kubelet의 max-pods args을 수정하거나, AWS VPC CNI 설정(ENABLE_PREFIX_DELEGATION, WARM_PREFIX_TARGET)을 수정 해야합니다.
k8s Service & AWS LoadBalancer Controller
쿠버네티스에서 파드는 일회성 리소스(’가축’의 개념)로 언제든지 생성되거나 삭제될 수 있기 때문에 파드에 지속적으로 연결하기 위한 변하지 않는 단일 진입점이 필요합니다. 이를 위해 파드에서 실행 중인 애플리케이션을 고정IP 주소와 포트를 통해 네트워크 서비스로 노출시키는 서비스(Service)를 사용하게 됩니다.
서비스에는 아래 3가지 타입이 존재합니다.
•
ClusterIP
: 별도로 타입을 지정하지 않는 경우 설정되는 기본값이며, 서비스에 클러스터 IP(내부 IP)를 할당합니다. 말 그대로 내부 IP이기 때문에 클러스터 내에서만 이 서비스에 접근이 가능합니다.
•
NodePort
: 서비스를 외부로 노출시키고자 할 때 사용되는 타입으로 외부에서 Node IP와 Port를 통해 접근이 가능하게 됩니다.
•
LoadBalancer
: 외부 IP를 가지고 있는 별도의 로드밸런서를 할당하는 타입입니다. 대부분의 클라우드 벤더사에서 제공하는 설정 방식입니다.
kops 실습 환경에서는 Service로 AWS LoadBalancer Controller를 사용합니다.
AWS LoadBalancer Controller를 서비스로 사용했을 때의 이점은 다른 타입과 같이 파드에 접근하기 위해서 노드포트 및 iptable을 거치지 않고 파드의 IP로 직접 통신(Bypass)하는 점입니다. 즉, 노드 포트, iptable를 타고 그에 맞는 파드로 라우팅하는 처리 과정이 생략되어 리소스를 절약할 수 있어 효율적입니다.
그렇다면 위에서 언급한 것처럼 동적으로 바뀌는 파드의 ip를 로드밸런서에서 알고 있어야하는데, 이를 AWS LoadBalancer Controller 파드가 담당합니다.
이 파드는 노드 내 파드의 IP를 포함한 여러 정보들을 주기적으로 수집해서 로드밸런서로 전송합니다.
아래와 같이 AWS LoadBalancer Controller를 배포하는 실습을 진행해보겠습니다. 먼저 AWSLoadBalancerController 배포를 위한 사전 설정을 진행합니다.
# AWSLoadBalancerController IAM 정책 생성
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.5/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
# EC2 instance profiles 에 IAM Policy 추가(attach)
ACCOUNT_ID=`aws sts get-caller-identity --query 'Account' --output text`
echo $ACCOUNT_ID
aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name masters.$KOPS_CLUSTER_NAME
aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name nodes.$KOPS_CLUSTER_NAME
# 생성 확인
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy
# kOps 내 cert-manager 설치
# 쿠버네티스 클러스터 내에서 TLS인증서를 자동으로 프로비저닝 및 관리하는 오픈 소스로 인증서 구성을 웹훅에 삽입하기 위해 필요합니다.
kops edit cluster --name ${KOPS_CLUSTER_NAME}
-----
spec:
certManager:
enabled: true
awsLoadBalancerController:
enabled: true
-----
kops update cluster --yes && echo && sleep 5 && kops rolling-update cluster
# 생성 확인
(jjikin:default) [root@kops-ec2 ~]# kubectl describe deploy -n kube-system aws-load-balancer-controller | grep Image | cut -d "/" -f 2
aws-alb-ingress-controller:v2.4.5@sha256:46029f2e2d48c55064b9eef48388bb6f5ec3390e6bf58790dd6b8d350e57ac90
(jjikin:default) [root@kops-ec2 ~]# kubectl get crd | grep elb
ingressclassparams.elbv2.k8s.aws 2023-03-18T11:40:35Z
targetgroupbindings.elbv2.k8s.aws 2023-03-18T11:40:36Z
Bash
NLB를 사용하는 서비스와 파드를 배포합니다.
# kubectl apply -f ~/pkos/2/echo-service-nlb.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-echo
spec:
replicas: 2
selector:
matchLabels:
app: deploy-websrv
template:
metadata:
labels:
app: deploy-websrv
spec:
terminationGracePeriodSeconds: 0
containers:
- name: akos-websrv
image: k8s.gcr.io/echoserver:1.5
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: svc-nlb-ip-type
annotations:
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
selector:
app: deploy-websrv
YAML
배포 시 아래와 같이 NLB와 연결된 TargetGroup이 생성되며 실제 파드에 할당된 Private IP가 TargetGroup에 매핑되어있음을 확인할 수 있습니다.
파드의 scale-out/in 테스트 시에도 즉각적으로 Targetgroup에서 initial/draining 확인이 가능했습니다.
위에서 언급한 것 처럼 파드의 Scale-out/in의 판단은 AWS LoadBalancer Controller 파드(이하 컨트롤러 파드)가 진행합니다.
이는 아래 방법과 권한을 통해 구현됩니다.
•
k8s에서 설정되어있는 컨트롤러 파드가 k8s api로 서비스의 엔드포인트의 정보를 확인합니다.
→ k8s ClusterRole
•
확인한 정보에 맞게 로드밸런서 타겟그룹에 파드를 추가 및 제거합니다.
→ EC2 Instance Profile에 추가한 AWSLoadBalancerControllerIAMPolicy
Ingress
인그레스(ingress)는 클러스터 외부에서 내부 서비스로 접근하는 HTTP, HTTPS 요청들을 처리하는 규칙들을 정의하고 Web Proxy 역할을 수행합니다.
동작 방식은 위의 NLB와 유사하게 동작하게 됩니다.
사전 설정은 원클릭 배포를 통해 이미 구성되어 있으므로 인그레스와 서비스, 파드를 배포합니다.
# kubectl apply -f ~/pkos/3/ingress1.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: game-2048
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: game-2048
name: deployment-2048
spec:
selector:
matchLabels:
app.kubernetes.io/name: app-2048
replicas: 2
template:
metadata:
labels:
app.kubernetes.io/name: app-2048
spec:
containers:
- image: public.ecr.aws/l6m2t8p7/docker-2048:latest
imagePullPolicy: Always
name: app-2048
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
namespace: game-2048
name: service-2048
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
type: NodePort
selector:
app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: game-2048
name: ingress-2048
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service-2048
port:
number: 80(jjikin:default)
YAML
배포 시 아래와 같이 ALB와 설정한 rule, 연결된 TargetGroup이 생성되며 실제 파드에 할당된 Private IP가 TargetGroup에 매핑되어있음을 확인할 수 있습니다.
ExternalDNS를 통해 개인 도메인에 매핑합니다.
# k8s-game2048.jjikin.com
WEBDOMAIN=k8s-game2048.$KOPS_CLUSTER_NAME
# 게임 파드와 Service, Ingress 배포
cat ~/pkos/3/ingress2.yaml | yh
WEBDOMAIN=$WEBDOMAIN envsubst < ~/pkos/3/ingress2.yaml | kubectl apply -f -
# ingress2.yaml
...
spec:
ingressClassName: alb
rules:
- host: ${WEBDOMAIN}
http:
paths:
- path: /
pathType: Prefix
...
Bash
# 리소스 삭제
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
Bash
과제
Ingress(with 도메인, 단일 ALB 사용)에 PATH /mario 는 mario 게임 접속하게 설정하고, /tetris 는 tetris 게임에 접속하게 설정하고, SSL(https strip) 적용해보기
먼저 ACM에서 사용할 도메인(*.jjikin.com)의 인증서를 발급받았습니다.
배포할 리소스가 정의된 yaml을 작성합니다.
# ~/pkos/3/ingress3.yaml
# 네임스페이스 재정의
apiVersion: v1
kind: Namespace
metadata:
name: gamebox
---
# 마리오 앱 배포
apiVersion: apps/v1
kind: Deployment
metadata:
name: mario
namespace: gamebox
spec:
replicas: 1
selector:
matchLabels:
app: mario
template:
metadata:
labels:
app: mario
spec:
containers:
- name: mario
image: pengbai/docker-supermario
---
apiVersion: v1
kind: Service
metadata:
name: mario
namespace: gamebox
annotations:
alb.ingress.kubernetes.io/healthcheck-path: /mario/index.html
spec:
selector:
app: mario
ports:
- port: 80
protocol: TCP
targetPort: 8080
type: NodePort
---
# 테트리스 앱 배포
apiVersion: apps/v1
kind: Deployment
metadata:
name: tetris
namespace: gamebox
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
namespace: gamebox
annotations:
alb.ingress.kubernetes.io/healthcheck-path: /tetris/index.html
spec:
selector:
app: tetris
ports:
- port: 80
protocol: TCP
targetPort: 80
type: NodePort
---
# 인그레스
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: gamebox
name: ingress-gamebox
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
# acm arn과 80->443 Redirect 되도록 annotation을 추가했습니다.
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-2:371604478497:certificate/23a4a9eb-7996-49c6-b0b0-4195f9f423a5
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:
- host: game.jjikin.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ssl-redirect
port:
name: use-annotation
- path: /mario
pathType: Prefix
backend:
service:
name: mario
port:
number: 80
- path: /tetris
pathType: Prefix
backend:
service:
name: tetris
port:
number: 80
Bash
•
http → https Redirection 적용
아래 링크를 참고하여 http로 접속 시 https로 자동 리다이렉션 되도록 설정했습니다.
리다이렉션 path는 반드시 맨 위에 작성해야 ALB Rule 내 최우선순위에 등록됩니다.
•
TargetGroup Unhealthy 문제
Ingress 설정 시 게임 애플리케이션 서비스 경로를 변경해줘야 정상 동작했습니다.
# 마리오
k exec -it $(kubectl get pod | grep mario | awk '{print $1}') -- bash -c "mkdir -p /usr/local/tomcat/webapps/ROOT/mario && mv /usr/local/tomcat/webapps/ROOT/* /usr/local/tomcat/webapps/ROOT/mario"
# 테트리스
k exec -it $(kubectl get pod | grep tetris | awk '{print $1}') -- bash -c "mkdir -p /usr/share/nginx/html/tetris && mv /usr/share/nginx/html/* /usr/share/nginx/html/tetris"`
# 에러는 무시
mv: cannot move '/usr/local/tomcat/webapps/ROOT/mario' to a subdirectory of itself, '/usr/local/tomcat/webapps/ROOT/mario/mario'
# 리소스 삭제
kubectl delete -f ingress3.yaml
Bash
참고
•
가시다님 PKOS2 스터디 자료
•
24단계 실습으로 정복하는 쿠버네티스