Search

Ingress

Tags
k8s
network
ingress
Created
2024/10/09 09:56
Created time
2024/10/09 00:56
category
kubernetes

개요

Service에서 배운 개념들을 7계층에서 제어할 수 있는 Ingress에 대해 탐색
현 글의 설명은 Nginx를 Ingress Controller를 이용한 것을 기준으로 작성

개념

Ingress낸 k8s 클러스터 내부의 Service들을 외부로 노출하는 Web Proxy의 역할을 수행
Ingress 자체는 API의 정의로만 되어 있고, Nginx와 같은 별도 구현체를 지정하여 기능을 처리

Service와의 차이

Service는 4계층에서 동작하지만, Ingress는 7계층에서 동작하여 HTTP/HTTPS 통신에서 다양한 기능을 제공

통신 흐름

k8s에서 통신은 아래와 같이 2가지 형태로 동작하는데, Ingress Controller에 NodePort 혹은 LoadBalancer를 적용하여 외부에서 클라이언트가 접속
클라이언트는 LB로 공개한 Port로 접근하여 Ingress Controller를 통해 Service의 통신으로 흐름이 이어짐 (Pod로 직접 보내는 것도 가능)
Ingress Controller는 주로 Nginx, Kong, Traefix와 같은 솔루션을 이용
각 솔루션 별 Ingress Controller 비교는 여기를 참고
워커 노드의 NodePort로 트래픽을 전달 후, iptables의 SEP 규칙에 따라 Pod로 분배하는 인스턴스 모드 (Ingress → Service → Application)
Ingress Controller Pod가 Kube API를 이용하여 Pod의 IP를 직접 전달 받아 타겟을 Pod의 IP로 설정하는 IP 모드 (Ingress → Application)

지원 기능

크게 제공하는 기능은 3가지로 HTTP/HTTPS 트래픽을 호스트 기반 라우팅으로 부하 분산, 카나리 트래픽 분배를 통한 유연한 업데이트, SSL 및 TLS 종료 (외부 Ingress Controller는 HTTPS, Ingress Controller Pod는 HTTP)를 지원

설정 파일 동기화

Ingress Controller의 목표 중하나는 nginx.conf라는 Nginx의 설정 파일을 자동으로 적용하는 것
ConfigMap에 Nginx 설정 파일을 작성하고 변경 사항이 발생하게 되면 LuaScript로 작성된 모듈을 통해 자동으로 Reload 되도록 구성
Reload가 필요한 상황은 아래와 같음
1.
Ingress가 새롭게 생성되었을 때
2.
기존에 생성된 Ingress에 TLS가 추가되었을 때
3.
Ingress에 작성된 애너테이션의 변경되었을 때
4.
Ingress의 path에 변경되었을 때
5.
Ingress / Service / Secret이 삭제되었을 때
6.
ingress-nginx 설정 시 애너테이션으로 설정이 불가능한 것은 ConfigMap으로 설정 가능

실습 환경 준비

AWS의 CloudFormation을 이용하며, Ingress Controller는 Pod로 동작하도록 생성하고 외부에서 해당 Pod로 접속할 수 있도록 별도의 NodePort를 구성

k8s 클러스터 생성

# CloudFormation 템플릿 다운로드 curl -sO https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/kans/kans-6w.yaml # 다운로드 파일 확인 ls -al | grep kans-6w.yaml # 템플릿으로부터 EC2 인스턴스 생성 aws cloudformation deploy --template-file kans-6w.yaml --stack-name mylab --parameter-overrides KeyName=jseo.d --region ap-northeast-2
Shell
복사
# EC2 퍼블릭 IP 확인 (해당 주소를 이용하여 ssh 원격 접속) aws cloudformation describe-stacks --stack-name mylab --query 'Stacks[*].Outputs[0].OutputValue' --output text --region ap-northeast-2
Shell
복사
# EC2 생성 여부 확인 while true; do date AWS_PAGER="" aws cloudformation list-stacks \ --stack-status-filter CREATE_IN_PROGRESS CREATE_COMPLETE CREATE_FAILED DELETE_IN_PROGRESS DELETE_FAILED \ --query "StackSummaries[*].{StackName:StackName, StackStatus:StackStatus}" \ --output table sleep 1 done
Shell
복사
# 여기서부터 SSH로 원격 접속 # Control Plane 노드에 Taint 설정 kubectl taint nodes k3s-s role=controlplane:NoSchedule
Shell
복사

Nginx Ingress Controller 생성

# Nginx Ingress Controller manifest 파일 생성 cat << EOT > ingress-nginx-values.yaml controller: service: type: NodePort nodePorts: http: 30080 https: 30443 nodeSelector: kubernetes.io/hostname: "k3s-s" metrics: enabled: true serviceMonitor: enabled: true EOT
Shell
복사
# ingress 네임스페이스 생성 kubectl create ns ingress # ingress-nginx 차트 설치 # https://github.com/kubernetes/ingress-nginx/blob/main/docs/deploy/index.md#bare-metal helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx -f ingress-nginx-values.yaml --namespace ingress --version 4.11.2
Shell
복사
# externalTrafficPolicy를 Local로 설정 kubectl patch svc -n ingress ingress-nginx-controller -p '{"spec":{"externalTrafficPolicy": "Local"}}'
Shell
복사

Nginx Ingress Controller 확인

# Ingress Controller 역할을 하는 Pod를 포함하여 ingress 네임스페이스의 모든 리소스 조회 kubectl get all -n ingress
Shell
복사
# 그 외 ServiceAccount, ConfigMap, Secret, Roles 부수 리소스 확인 kubectl get sa,cm,secret,roles -n ingress
Shell
복사
# nginx.conf 설정 파일 확인 kubectl exec deploy/ingress-nginx-controller -n ingress -it -- cat /etc/nginx/nginx.conf
Shell
복사
# Nginx Ingress Controller의 버전 확인 POD_NAMESPACE=ingress POD_NAME=$(kubectl get pods -n $POD_NAMESPACE -l app.kubernetes.io/name=ingress-nginx --field-selector=status.phase=Running -o name) kubectl exec $POD_NAME -n $POD_NAMESPACE -- /nginx-ingress-controller --version
Shell
복사

통신 흐름

위와 같은 도식이 될 수 있도록 사전에 구성한 Ingress Controller 외에도 클러스터에서 제공할 Pod와 NodePort 및 ClusterIP 타입의 Service를 생성

리소스 생성

# svc1 : ClusterIP # deploy1 : nginx cat << EOT > svc1-pod.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deploy1-websrv spec: replicas: 1 selector: matchLabels: app: websrv template: metadata: labels: app: websrv spec: containers: - name: pod-web image: nginx --- apiVersion: v1 kind: Service metadata: name: svc1-web spec: ports: - name: web-port port: 9001 targetPort: 80 selector: app: websrv type: ClusterIP EOT
Shell
복사
# svc2 : NodePort # deploy2 : sample image cat << EOT > svc2-pod.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deploy2-guestsrv spec: replicas: 2 selector: matchLabels: app: guestsrv template: metadata: labels: app: guestsrv spec: containers: - name: pod-guest image: gcr.io/google-samples/kubernetes-bootcamp:v1 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: svc2-guest spec: ports: - name: guest-port port: 9002 targetPort: 8080 selector: app: guestsrv type: NodePort EOT
Shell
복사
# svc3 : ClusterIP # deploy3 : echoserver cat << EOT > svc3-pod.yaml apiVersion: apps/v1 kind: Deployment metadata: name: deploy3-adminsrv spec: replicas: 3 selector: matchLabels: app: adminsrv template: metadata: labels: app: adminsrv spec: containers: - name: pod-admin image: k8s.gcr.io/echoserver:1.5 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: svc3-admin spec: ports: - name: admin-port port: 9003 targetPort: 8080 selector: app: adminsrv EOT
Shell
복사
# 각 manifest들을 적용 kubectl apply -f svc1-pod.yaml,svc2-pod.yaml,svc3-pod.yaml
Shell
복사
# 리소스 생성 확인 kubectl get po,svc,ep
Shell
복사

Ingress 생성

# Ingress 생성 # root (/), guest, admin 경로가 존재하며 각각이 1,2,3번의 Service들을 지칭 cat << EOT > ingress1.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingress-1 annotations: #nginx.ingress.kubernetes.io/upstream-hash-by: "true" spec: ingressClassName: nginx rules: - http: paths: - path: / pathType: Prefix backend: service: name: svc1-web port: number: 80 - path: /guest pathType: Prefix backend: service: name: svc2-guest port: number: 8080 - path: /admin pathType: Prefix backend: service: name: svc3-admin port: number: 8080 EOT # manifest 적용 kubectl apply -f ingress1.yaml
Shell
복사
# Ingress 생성 확인 kubectl get ing -A kubectl describe ing/ingress-1
Shell
복사
# nginx.conf 설정 확인 시 location 항목을 보면 변경 사항이 발생한 것을 확인 가능 kubectl exec deploy/ingress-nginx-controller -n ingress -it -- cat /etc/nginx/nginx.conf | grep -E 'location.*(/|/guest/|/admin/) {' -A3
Shell
복사

접속 확인

# 서비스 별 접속 URI 확인 echo -e "Ingress1 sv1-web URL = http://$(curl -s ipinfo.io/ip):30080" echo -e "Ingress1 sv2-guest URL = http://$(curl -s ipinfo.io/ip):30080/guest" echo -e "Ingress1 sv3-admin URL = http://$(curl -s ipinfo.io/ip):30080/admin"
Shell
복사
# 좌측 터미널 # (kubetail 패키지가 설치되어 있단 전제하에) Nginx로 접속한 IP 주소를 포함한 로그를 출력 kubetail -n ingress -l app.kubernetes.io/component=controller # 우측 터미널 # IP 주소 변수 할당 MYIP=<EC2 공인 IP>
Shell
복사
# 우측 터미널 # svc1 접속 (80 포트) curl -s $MYIP:30080
Shell
복사
클러스터가 설치된 서버에서 요청을 전송했기 때문에 클라이언트 주소가 서버의 공개 IP 주소로 남아 있고, 요청을 처리한 목적지는 Pod의 IP 주소로 남은 것을 확인 가능
# 우측 터미널 # svc2 반복 접속 및 통계 확인 (8080 포트) for i in {1..100}; do curl -s $MYIP:30080/guest ; done | sort | uniq -c | sort -nr
Shell
복사
적절히 Service를 통해 부하 분산되어 각 Pod가 고르게 요청을 처리한 것을 확인 가능
# 우측 터미널 # svc3 반복 접속 및 Hostname 통계 확인 (8080 포트) for i in {1..100}; do curl -s $MYIP:30080/admin | grep Hostname ; done | sort | uniq -c | sort -nr # svc3 반복 접속 및 클라이언트 통계 확인 (8080 포트) for i in {1..100}; do curl -s $MYIP:30080/admin | grep -E '(client_address|x-forwarded-for)' ; done | sort | uniq -c | sort -nr
Shell
복사
이전과 마찬가지로 부하 분산이 고르게 된 것을 볼 수 있고, X-Forwarded-For라는 실제 클라이언트 IP 주소와 Ingress Controller에서 클러스터의 특정 노드로 교체된 IP 주소를 확인 가능

XFF (X-Forwarded-For)

XFF는 X-Forwarded-For의 약어로 HTTP 헤더로 사용되는 키 중 하나이고, 클라이언트의 IP 주소를 식별하기 위한 헤더
RFC 표준으로 존재하는 헤더는 아니지만 가장 표준처럼 사용하는 헤더이고, 솔루션에 따라 X-Real-IP를 사용하거나 X-Forwarded-Proto를 이용하는 경우도 존재
일반적으로 WS/WAS 앞에 LB 혹은 캐싱 서버, 프록시 서버 등이 존재하게 되면, 해당 서버들이 요청을 먼저 받아서 WS/WAS로 전달하고, WS/WAS에서 요청을 처리하면 해당 서버들이 다시 받아서 클라이언트에게 전달하는 방식으로 동작
따라서 WS/WAS에서 클라이언트의 주소를 얻고자 request.getRemoteAddr과 같은 메서드를 수행하더라도 원하는 IP 주소를 얻을 수가 없기에 XFF 같은 헤더를 이용하게 된 것
요약하면 Source의 주소가 변환되는 과정에서 원본 Source를 기록해두기 위한 헤더
XFF는 X-Forwarded-For: <client>, <proxy1>, <proxy2>… 로 표기
XFF는 여러 프록시 서버를 경유하는 경우에 요청이 통과하는 각 프록시 서버의 IP 주소를 클라이언트 필드 뒤에 열거
가장 오른쪽의 IP 주소는 가장 최근의 프록시의 IP 주소이고 가장 왼쪽의 IP 주소는 원래 클라이언트의 IP 주소

호스트 기반 라우팅

위 실습에서는 Ingress 접속을 도메인 이름이 아닌 IP 주소로 접속했었는데, Ingress Controller Pod에 설정한 규칙에 따라 도메인 이름의 매칭으로 특정 서비스에 트래픽 전달 가능

Ingress 생성

# Ingress manifest 생성 cat << EOT > ingress2.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingress-2 spec: ingressClassName: nginx rules: - host: kans.com http: paths: - path: / pathType: Prefix backend: service: name: svc3-admin port: number: 8080 - host: "*.kans.com" http: paths: - path: /echo pathType: Prefix backend: service: name: svc3-admin port: number: 8080 EOT # IP를 변수에 할당 MYIP=<EC2 공인 IP> # 도메인을 변수에 할당 # MYDOMAIN1=jseo.com # MYDOMAIN2=test.jseo.com MYDOMAIN1=<custom-domain> MYDOMAIN2=<custom-domain> # 설정 파일의 도메인을 변수에 할당한 값으로 치환 sed -i "s/kans.com/$MYDOMAIN1/g" ingress2.yaml # 위 manifest와 이전 실습에서 사용했던 svc3-pod를 적용 kubectl apply -f ingress2.yaml,svc3-pod.yaml
Shell
복사
# 도메인 이름을 /etc/hosts에 반영 # 아래와 같이 /etc/hosts 조작이 없으려면, curl에 Host 헤더를 직접 명시 echo "$MYIP $MYDOMAIN1" | sudo tee -a /etc/hosts echo "$MYIP $MYDOMAIN2" | sudo tee -a /etc/hosts
Shell
복사
# Ingress 생성 확인 kubectl describe ingress ingress-2 | sed -n "5, \$p"
Shell
복사

접속 확인

# 좌측 터미널 # (kubetail 패키지가 설치되어 있단 전제하에) Nginx로 접속한 IP 주소를 포함한 로그를 출력 kubetail -n ingress -l app.kubernetes.io/component=controller
Shell
복사
# 우측 터미널 # IP 주소 이용 시 Ingress에 설정한 것과 상이한 Host 헤더로 요청되어 접속 실패 curl $MYIP:30080/echo -v
Shell
복사
# 우측 터미널 # 도메인 이름을 이용 시 Ingress에 설정한대로 요청되어 정상 접속 curl $MYDOMAIN2:30080/echo -v
Shell
복사

리소스 삭제

kubectl delete deploy,svc,ing --all
Shell
복사

카나리 트래픽 분배

카나리 배포는 신규 버전을 적용할 때 이상 동작 여부를 모니터링하면서 문제가 없는 경우에 버전을 확대 적용하는 방식
100%의 트래픽이 들어온다고 했을 때, 90% 트래픽을 기존 버전, 10%의 트래픽을 신규 버전으로 유입하여 문제가 없다면 비중을 점점 80:20, 70:30 … 0:100으로 맞춰가는 것
이와 유사하게 트래픽을 비율로 분배하는 기능을 카나리 트래픽 분배라고 하며, 해당 기능을 사용하기 위해 Ingress Controller에 Nginx 관련 애너테이션 설정이 필요

manifest 생성

# svc1-pod manifest 생성 cat << EOT > canary-svc1-pod.yaml apiVersion: apps/v1 kind: Deployment metadata: name: dp-v1 spec: replicas: 3 selector: matchLabels: app: svc-v1 template: metadata: labels: app: svc-v1 spec: containers: - name: pod-v1 image: k8s.gcr.io/echoserver:1.5 ports: - containerPort: 8080 terminationGracePeriodSeconds: 0 --- apiVersion: v1 kind: Service metadata: name: svc-v1 spec: ports: - name: web-port port: 9001 targetPort: 8080 selector: app: svc-v1 EOT
Shell
복사
# svc2-pod manifest 생성 cat << EOT > canary-svc2-pod.yaml apiVersion: apps/v1 kind: Deployment metadata: name: dp-v2 spec: replicas: 3 selector: matchLabels: app: svc-v2 template: metadata: labels: app: svc-v2 spec: containers: - name: pod-v2 image: k8s.gcr.io/echoserver:1.6 ports: - containerPort: 8080 terminationGracePeriodSeconds: 0 --- apiVersion: v1 kind: Service metadata: name: svc-v2 spec: ports: - name: web-port port: 9001 targetPort: 8080 selector: app: svc-v2 EOT
Shell
복사
# 카나리 설정이 없는 1번 ingress의 manifest 생성 cat << EOT > canary-ingress1.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingress-canary-v1 spec: ingressClassName: nginx rules: - host: kans.com http: paths: - path: / pathType: Prefix backend: service: name: svc-v1 port: number: 8080 EOT
Shell
복사
# 카나리 설정이 반영된 2번 ingress의 manifest 생성 cat << EOT > canary-ingress2.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingress-canary-v2 annotations: nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-weight: "10" spec: ingressClassName: nginx rules: - host: kans.com http: paths: - path: / pathType: Prefix backend: service: name: svc-v2 port: number: 8080 EOT
Shell
복사

Pod의 버전 확인

# echoserver:1.5는 nginx 1.13.0을 이용 for pod in $(kubectl get pod -o wide -l app=svc-v1 |awk 'NR>1 {print $6}'); do curl -s $pod:8080 | egrep '(Hostname|nginx)'; done # echoserver:1.6은 nginx 1.13.1을 이용 for pod in $(kubectl get pod -o wide -l app=svc-v2 |awk 'NR>1 {print $6}'); do curl -s $pod:8080 | egrep '(Hostname|nginx)'; done
Shell
복사

Ingress 생성

# 사용할 도메인을 변수에 할당 MYDOMAIN1=jseo.com # ingress의 manifest 파일의 도메인 이름을 수정 sed -i "s/kans.com/$MYDOMAIN1/g" canary-ingress1.yaml sed -i "s/kans.com/$MYDOMAIN1/g" canary-ingress2.yaml # ingress 및 리소스 생성 kubectl apply -f canary-svc1-pod.yaml,canary-svc2-pod.yaml,canary-ingress1.yaml,canary-ingress2.yaml
Shell
복사

카나리 트래픽 분배 확인

# 접속 테스트 # ingress에 설정했던 것처럼 약 9:1의 비율로 트래픽 분배 for i in {1..1000}; do curl -s $MYDOMAIN1:30080 | grep nginx ; done | sort | uniq -c | sort -nr
Shell
복사
# 비율 수정 # 애너테이션에 설정했던 canary-weight를 설정 kubectl annotate --overwrite ingress ingress-canary-v2 nginx.ingress.kubernetes.io/canary-weight=50 # 접속 테스트 # 위에서 수정한 것처럼 트래픽이 반씩 거의 균등하게 분배
Shell
복사
kubectl annotate --overwrite ingress ingress-canary-v2 nginx.ingress.kubernetes.io/canary-weight=100
kubectl annotate --overwrite ingress ingress-canary-v2 nginx.ingress.kubernetes.io/canary-weight=0
위 2가지 방식으로 설정을 변경하게 되면 한 쪽으로만 트래픽이 쏠리게 됨 (항상 100:0의 비율의 트래픽이 분배되는 것은 아님)

리소스 삭제

kubectl delete deploy,svc,ing --all
Shell
복사

SSL/TLS 종료

외부에서 Ingress Controller로의 트래픽은 HTTPS 접속, 내부로의 Ingress Controller에서 Pod까지의 트래픽은 HTTP 접속을 지원

manifest 생성

# 리소스 manifest 생성 cat << EOT > svc-pod.yaml apiVersion: v1 kind: Pod metadata: name: pod-https labels: app: https spec: containers: - name: container image: k8s.gcr.io/echoserver:1.6 terminationGracePeriodSeconds: 0 --- apiVersion: v1 kind: Service metadata: name: svc-https spec: selector: app: https ports: - port: 8080 EOT
Shell
복사
# secretName과 tls가 적용된 ingress의 manifest를 생성 cat << EOT > ssl-termination-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: https spec: ingressClassName: nginx tls: - hosts: - kans.com secretName: secret-https rules: - host: kans.com http: paths: - path: / pathType: Prefix backend: service: name: svc-https port: number: 8080 EOT
Shell
복사

Secret 및 Ingress 생성

# 도메인 이름 변수 할당 MYDOMAIN1=jseo.com # 도메인 이름을 manifest에서 치환 sed -i "s/kans.com/$MYDOMAIN1/g" ssl-termination-ingress.yaml # manifest 적용 kubectl apply -f svc-pod.yaml,ssl-termination-ingress.yaml
Shell
복사
# 인증서 생성 openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=$MYDOMAIN1/O=$MYDOMAIN1" # 생성된 파일 확인 tree # Secret 생성 kubectl create secret tls secret-https --key tls.key --cert tls.crt # Secret 확인 kubectl get secrets secret-https
Shell
복사

SSL Termination 확인

SSL Termination을 확인하기 위해선 로우 레벨 분석이 필요한데 Flannel은 Calico만큼의 구체적인 정보 표기를 하지 않음
실습은 Flannel로 정리했으나, Calico CNI를 이용하는 것을 기준으로 SSL Termination 확인 방법을 정리
# 우측 터미널 # ingress-nginx-controller가 동작하고 있는 노드를 확인 kubectl get po -o wide -A | grep ingress-nginx-controller # 찾은 노드로 이동하여 접속 ssh vagrant@<node> # ingress-nginx-controller Pod의 인터페이스 확인 C_INTERFACE=$(calicoctl get wep -n ingress | grep ingress | awk '{print $5}') # HTTP NodePort와 HTTPS NodePort를 변수에 할당 export ING_HTTP=$(kubectl get service -n ingress ingress-nginx-controller -o jsonpath='{.spec.ports[0].nodePort}') export ING_HTTPS=$(kubectl get service -n ingress ingress-nginx-controller -o jsonpath='{.spec.ports[1].nodePort}') # tcpdump를 수행 tcpdump -i $C_INTERFACE -nn tcp port 80 or tcp port 443 or tcp port 8080 or tcp port $ING_HTTP or tcp port $ING_HTTPS -w /tmp/ingress.pcap
Shell
복사
# 좌측 터미널 # /etc/hosts 수정 없이 접속 확인 curl -Lk -H "host: $MYDOMAIN1" https://$MYDOMAIN1:30443
Shell
복사

리소스 삭제

# 실습 리소스 삭제 kubectl delete po,svc,ing --all # Ingress Controller 삭제 helm uninstall -n ingress ingress-nginx
Shell
복사

실습 환경 삭제

aws cloudformation delete-stack --stack-name mylab
Shell
복사

Reference