Switching Routing


ip link

서로 다른 Host 가 통신하기 위해선 두 Host 를 이어주는 Switch 가 필요하다. Host 를 Switch 에 연결하려면 Network Interface 가 필요한데, 위 명령어를 통해 Host 의 Network Interface 를 확인할 수 있다. Linux 의 경우 eth0 라는 Network Interface 를 확인할 수 있다.

ip addr
ip addr add 192.168.1.10/24 dev eth0

Host 들이 Switch 에 연결된 이후 위 명령어를 통해 Host 에 IP 주소를 할당해줄 수 있다.

ip route
ip route add 192.168.2.0/24 via 192.168.1.1
ip route add default via 192.168.2.1
ip route add 0.0.0.0 via 192.168.2.1

서로 다른 Switch 가 통신하기 위해선 두 Switch 를 이어주는 Router 가 필요하다. 192.168.2.0/24 의 주소를 가지는 Switch 에 접근하기 위해선 Router 의 192.168.1.1 주소를 거쳐가면 된다는 의미다. default 또는 0.0.0.0 는 이외 모든 주소를 의미하며 즉, 인터넷에 접근하기 위해선 192.168.2.1 주소를 거쳐가야 된다는 의미다.

cat /proc/sys/net/ipv4/ip_forward
echo 1 > /proc/sys/net/ipv4/ip_forward

Router 가 192.168.1.1192.168.2.1 의 주소를 가지고 있다는 것은 eth0eth1 이 할당됐다는 의미이고, 기본적으로 서로 다른 Network Interface 간에 Packet 전달은 막혀있다. 위 명령어를 통해 ip_forward 값을 1 로 바꿔주면 Router 내부에서 Packet 를 Network Interface 간에 전달할 수 있도록 해줄 수 있다. 재부팅할 경우 설정값이 초기화 되기 때문에 /etc/sysctl.conf 에서 net.ipv4.ip_forward 값을 1 로 설정해주면 영구적으로 설정할 수도 있다.

DNS


cat > /etc/hosts
192.168.1.10    web
192.168.1.11    db
192.168.1.15    web-1
192.168.1.16    db-1
192.168.1.21    web-2
192.168.1.22    db-2

IP 주소 대신 Name 으로 Host 를 식별하기 위해 위 경로에 IP 주소에 해당하는 Host Name 을 지정해 줄 수 있다. 대신 모든 Host 에 필요하기 때문에 관리하기가 어려워진다. 때문에 DNS 서버를 활용해 하나의 관리포인트로 통합한다.

cat /etc/resolv.conf

DNS 서버의 주소는 위 경로에 저장하여 사용할 수 있다. 8.8.8.8 은 Google 이 제공하는 DNS 서버다.

cat /etc/nsswitch.conf
 
...
hosts: files dns
...

기본적으로 /etc/hosts 에서 주소를 찾고 없으면 /etc/resolv.conf 에서 주소를 찾는데, 위 설정파일에서 순서를 변경할 수도 있다.

Network Namespaces


ip netns add red
ip netns add blue

Container 가 namespace 를 통해 프로세스를 격리하듯 Network 역시 격리할 수 있다. 위 명령어는 redblue 라는 이름을 가진 Network Namespace 를 생성한다는 의미다.

ip netns exec red ip link
ip -n red link

ip link 명령어를 통해 Network Interface 를 확인했듯 Network Namespace 의 Network Interface 를 확인하기 위해선 위 명령어를 사용할 수 있다.

ip -n red arp
ip -n red route

arp 와 route 역시 동일한 방법으로 확인할 수 있다.

Virtual Cable

ip link add veth-red type veth peer name veth-blue

위 명령어로 가상 케이블을 생성하고,

ip link set veth-red netns red
ip link set veth-blue netns blue
 
ip -n red addr add 192.168.15.1/24 dev veth-red
ip -n blue addr add 192.168.15.2/24 dev veth-blue
 
ip -n red link set veth-red up
ip -n blue link set veth-blue up

위 명령어로 Network Namespace 끼리 연결해줄 수 있다.

Linux Bridge

ip link add v-net-0 type bridge
ip link set dev v-net-0 up

Network Namespace 가 많아질 경우 Switch 역할을 하는 Linux Bridge 를 활용할 수 있다.

ip link add veth-red type veth peer name veth-red-br
ip link set veth-red netns red
ip link set veth-red-br master v-net-0
 
ip -n red addr add 192.168.15.1 dev veth-red
ip -n red link set veth-red up

이후 Network Namespace 와 Bridge 를 연결하기 위한 Virtual Cable 을 생성 및 할당해주고, IP 주소 도 할당해 준 뒤 활성화 해주자.

ip addr add 192.168.15.5/24 dev v-net-0

위 명령어를 통해 Host 와 Bridge 를 연결해주자.

ip netns exec blue ip route add 192.168.1.0/24 via 192.168.15.5

Namespace 가 외부 Host 와 통신하기 위해선 외부에 192.168.1.0/24 의 주소를 가지는 Switch 와 routing 설정을 해줘야한다. 위 명령어를 사용하면 192.168.15.5 의 주소를 가지는 v-net-0 을 통해 호스트의 eth0 를 거쳐 외부로 향하게 된다.

iptables -t nat -A PREROUTING --dport 80 --to-destination 192.168.15.2:80 -j DNAT

외부 Host 가 다른 Host 의 Network Namespace 에 접근하기 위해선 위 명령어로 포트포워딩 해줘야 한다.

Docker Networking


ip link

08. Docker Networking 에서 배운 bridge 가 바로 Linux Bridge 다. ip link 로 확인해보면 docker0 라는 이름을 가진 Network Interface 를 확인할 수 있다.

ip netns

Docker Container 를 실행하면 Network Namespace 도 마찬가지로 자동으로 생성된다.

iptables -nvL -t nat

포트포워딩도 마찬가지. Docker Container 를 실행할 때 iptable 설정까지 자동으로 생성된다.

즉, Docker Container 를 생성할 때, 네트워크 측면에선 내부적으론 위에서 언급한 모든 작업들이 자동으로 수행되던 것이다…

Pod Networking


서로 다른 Node 에 위치한 Pod 간 통신을 활성화 하기 위해선 아래와 같은 작업이 필요하다.

ip link add v-net-0 type bridge
ip link set dev v-net-0 up
ip addr add 10.244.1.1/24 dev v-net-0

먼저 각 Node 마다 Bridge 를 생성해준 뒤,

ip route add 10.244.2.2 via 192.168.1.12

다른 Node 의 Bridge 로 향하는 Route 를 설정해준다. 혹은 라우터를 활용해 Routing 설정을 한 곳에서 관리하는 방법도 있겠다.

Container 가 생성될 때 마다 Node 에 하나하나 접속해서 위 명령어들을 실행해야한다. Node 가 많아지고 네트워킹이 복잡해지면 해당 작업이 번거로워지기 때문에 자동화된 스크립트를 활용할 수 있다.

# net-script.sh
ADD)
  # Create veth pair
  # Attach veth pair
  # Assign IP Address
  # Bring Up Interface
DEL)
  # Delete veth pair

위와 같은 기준으로 작성된 스크립트를 활용해 Container 가 생성될 때 마다 실행해주면 Pod 간 네트워킹을 자동화할 수 있다. 위 기준을 CNI 가 제공하고 다양한 CNI Plugin 들이 구현된 스크립트를 제공한다.

Cluster Networking


K8s 도 결국 Master 와 Worker Node 간의 통신이 가능해야 함으로 각 Component 마다 위와 같은 Port 들을 열어줘야 한다. 자세한 내용은 공식 문서를 참고하자.

CNI


쿠버네티스는 기본적으로 Pod 간의 통신을 지원하지 않는다. 때문에 Pod 간 통신이 가능하도록 하려면 CNI Plugin 을 추가적으로 설치해 사용해야한다. CNI 는 쿠버네티스에서 Pod 에 IP 를 부여하고, 네트워크 연결을 실제로 구성해주는 핵심 네트워크 엔진이다.

ExecStart=/usr/local/bin/kubelet \
  ...
  --network-plugin=cni \
  --cni-bin-dir=/opt/cni/bin \
  --cni-conf-dir=/etc/cni/net.d \
  ...

CNI 를 설정해주기 위해선 kubelet 을 실행할 때 옵션으로 넘겨주면된다.

ls /opt/cni/bin

위 경로에서 사용할 수 있는 모든 CNI Plugin 들을 확인할 수 있다.

ls /etc/cni/net.d

위 경로에 존재하는 파일을 바탕으로 CNI Plugin 를 설정할 수 있다.

cat /etc/cni/net.d/10-flannel.conflist
{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

Weave CNI

kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"

Weave 는 CNI Plugin 중 하나인 솔루션으로 위와 같이 Pod 로 생성해서 운영할 수 있다.

Weave CNI - IP Address Management

cat /etc/cni/net.d/net-script.conflist
{
  "cniVersion": "0.2.0",
  "name": "mynet",
  "type": "net-script",
  "bridge": "cni0",
  "isGateway": true,
  "ipMasq": true,
  "ipam": {
      "type": "host-local",
      "subnet": "10.244.0.0/16",
      "routes": [
          {
              "dst": "0.0.0.0/0"
          }
	  ]
  }
}

Pod 간 할당되는 IP 를 관리하기 위해 CNI 에선 DHCPhost-local 이라는 빌트인 플러그인을 제공한다. 해당 설정 역시 위 파일에서 관리할 수 있다.

NetworkPolicy


apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:
    matchLabels:
      role: db # role=db 인 레이블을 가진 Pod 에 대한 NetworkPolicy
  policyTypes:
  - Ingress # Ingress 제한 (ingress 필드가 없으면 Ingress 를 모두 제한)
  - Egress  # Egress 제한 (egress 필드가 없으면 Egress 를 모두 제한)
  ingress:
  - from:
    - podSelector:       # 같은 네임스페이스의 role=api-pod 레이블을 가진 Pod 에서 오는 트래픽 허용
        matchLabels:
          role: api-pod
    - namespaceSelector: # prod 네임스페이스의 모든 Pod 에서 오는 트래픽 허용
        matchLabels:
          name: prod
    - ipBlock:           # 192.168.5.10 에서 오는 트래픽 허용
        cidr: 192.168.5.10/32
    ports: # 위 from 에 해당하는 곳에서 TCP 3306 포트로 들어오는 트래픽만 허용
    - protocol: TCP
      port: 3306
  egress:
  - to:
    - ipBlock: # 192.168.5.10 로 보내는 트래픽 허용
        cidr: 192.168.5.10/32
    ports: # 목적지(192.168.5.10)의 TCP 80 으로 나가는 트래픽 허용
    - protocol: TCP
      port: 80

쿠버네티스는 기본적으로 클러스터에 속한 모든 Pod 간의 통신을 허용한다. 만약 Ingress 와 Egress 를 제한하고 싶다면 NetworkPolicy 를 생성하자.

트래픽 차단/허용은 CNI 가 구현하기 때문에 CNI Plugin 이 NetworkPolicy 를 지원해야 한다.

  • Calico, Cilium 은 지원 / Flannel 은 미지원
  • 지원하지 않는 CNI 환경에선 NetworkPolicy 가 있어도 적용되지 않는다.

podSelector, namespaceSelector. ipBlock 등 다양한 기준으로 트래픽을 제어할 수 있다.

  • podSelector: 같은 namespace 내 특정 Pod 만 적용
  • namespaceSelector: 특정 namespace 전체를 대상으로 적용
  • ipBlock: 특정 IP 를 대상으로 적용

Service


Service 는 여러 개의 Pod 로 구성된 애플리케이션을 하나의 네트워크 엔드포인트로 외부에 노출시키는 리소스이다.

  • Pod 는 언제든지 재시작되거나 재배포될 수 있기 때문에 IP 가 항상 바뀔 수 있다.
  • Service 는 클러스터 내부에 고정된 가상 IP 를 외부에 노출시켜 클라이언트가 항상 같은 IP 주소로 파드에 접근할 수 있도록 한다.
  • Label Selector 를 이용해 트래픽을 전달해야할 Pod 를 지정할 수 있다.
  • 내부적으로 각 노드에서 실행되는 kube-proxy 가 Service 의 가상 IP 로 들어온 요청을 Label Selector 조건에 해당하는 Pod 중 하나로 전달한다.
  • 여러가지 Service Type 을 가지고 있다.
    • ClusterIP
    • NodePort
    • LoadBalancer 등

Service Networking

K8s 에선 Pod 끼리 통신하는 대신 Service 를 통해 Cluster 내 Pod 간의 통신 또는 외부와의 통신을 제공한다. Pod 이 namespace 등을 할당 받아 실제로 존재하는 것과 달리, Service 는 kube-proxy 에 의해 Cluster 에 걸쳐 Forwarding Rule 을 가지고 생성되는 Virtual Object 다.

kube-proxy --proxy-mode iptables # 'userspace | iptables | ipvs'

kube-proxy 를 실행할 때 proxy-mode 옵션을 통해 Forwarding Rule 을 관리할 버전을 설정할 수 있다.

kube-api-server --service-cluster-ip-range ipNet (Default: 10.0.0.0/24)

kube-api-server 를 실행할 때 Service 가 가질 수 있는 IP range 를 지정해줄 수 있다. Service IP range 와 Pod IP range 는 겹치면 안된다는 것도 참고하자.

iptables -L -t nat | grep db-service

iptables 모드로 생성한 Service 의 Forwarding Rule 을 위 명령어를 통해 확인할 수 있다.

Service 흐름

클러스터 내부에서 Service 를 호출할 때:

  1. 클라이언트가 Service 의 ClusterIP:Port 로 요청을 보낸다.
  2. kube-proxy 가 이 요청을 가로챈다.
  3. EndpointSlice 를 참조해 적절한 Pod IP 로 라우팅한다.
  4. Pod 는 targetPort 에서 요청을 받는다.

ClusterIP

apiVersion: v1
kind: Service
metadata:
  name: back-end
spec:
  type: ClusterIP # default 값
  selector: # label 기준으로 트래픽을 보낼 pod 결정
    app: myapp
    type: back-end
  ports:
   - port: 8080 # (required) 클라이언트가 Service 에 요청을 보낼 port
     targetPort: 80 # 실제 pod 에서 리스닝하는 port, 지정하지 않으면 port 값을 따름

ClusterIP 예시로 들면, back-end:8080 으로 들어온 요청이 label 이 app=myapp, type=back-end 인 Pod 의 80 포트로 전달된다.

  • ClusterIP 는 클러스터 내부에서만 접근 가능한 IP 로 기본값으로 적용된다.
  • 클러스터 내부에서 서비스 연결은 DNS 를 이용
  • 가령 3-tier-architecture 에서 backend app 에 요청을 보내고 싶을 때 backend 를 담당하는 여러 Pod 가 cluster 전체에 퍼져있으니 ClusterIP 를 통해 원하는 layer 를 지정

NodePort

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: NodePort
  selector: # label 기준으로 트래픽을 보낼 pod 결정
    app: myapp
    type: front-end
  ports:
   - port: 80 # (required) 클라이언트가 Service 에 요청을 보낼 port
     targetPort: 80 # 실제 pod 에서 리스닝하는 port, 지정하지 않으면 port 값을 따름
     nodePort: 30008 # 30000-32767 범위, 지정하지 않을 경우 자동으로 할당됨
  • 모든 노드에 동일한 고정 포트(30000-32767)를 생성하여 외부에 노출한다.
  • 외부에서 {Node IP}:{nodePort} 형식으로 접근할 수 있다.
  • NodePort 역시 ClusterIP 를 가지기 때문에 내부적으로는 ClusterIP 를 통해 Pod 에 접근한다.
kubectl run httpd --image=httpd --port=80 --expose
  • kubectl run 으로 Pod 를 생성할 때 —expose 옵션을 주면 Pod 에 연결되는 ClusterIP 를 자동으로 생성해준다. ClusterIP 와 연결되어야 하기 때문에 —port 옵션이 필수적으로 포함되어야 한다.

LoadBalancer

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: LoadBalancer
  ports:
   - port: 80
     targetPort: 80
     nodePort: 30008
  • AWS, Azure, GCP 등에서 제공하는 Native Load Balancer 로 부하를 분산하여 보내는 역할을 수행한다.
  • 내부적으로 NodePort 기반으로 작동한다.

ExternalName

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: ExternalName
  externalName: myapp-service.example.com
  • 외부 DNS 이름으로 라우팅 CNAME 처럼 작동한다.
  • 프록시가 아닌 DNS 레벨에서 리다이렉션

Service 생성

kubectl expose deployment nginx --port 80

CoreDNS


<pod-ip-address>.<namespace-name>.pod.cluster.local
<service-name>.<namespace-name>.svc.cluster.local

K8s 에서 Pod 과 Service 는 위 규칙에 따라 Domain Name 을 할당 받는다.

Pod 과 Service 들이 서로 Domain Name 으로 통신하기 위해 DNS 를 활용하는데, K8s 에선 CoreDNS 를 기반으로 한다. CoreDNS 는 기본적으로 Deployment 형태로 배포되며 Service 역시 제공해 Pod 과 Service 들이 kube-dns Service 를 향해 nslookup 을 수행할 수 있다.

kubectl get configmap -n kube-system
kubectl describe cm -n kube-system

CoreDNS 의 설정은 ConfigMap 으로 구성 가능하다.

cat /var/lib/kubelet/config.yaml | grep -A2 clusterDNS
clusterDNS:
- 10.96.0.10
clusterDomain: cluster.local

CoreDNS 는 kubelet 으로 부터 배포되기에 kubelet 의 설정파일에서 CoreDNS 의 IP 를 확인할 수 있다.

kubectl run -it --rm --restart=Never test-pod --image=busybox -- cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
pod "test-pod" deleted

Pod 내부에 있는 resolv.conf 파일에서 CoreDNS IP 가 적용되어 있는 것을 확인할 수 있다.

CoreDNS vs ExternalDNS

CoreDNS 는 클러스터 내부의 DNS 질의를 처리하는 내부 DNS 서버이다. ExternalDNS 는 Ingress 나 Service 정보를 외부 DNS 제공자의 레코드를 자동으로 생성 및 동기화하는 컨트롤러이다.

CoreDNS

<service-name>.<namespace-name>.svc.cluster.local 등 클러스터 도메인에 DNS 질의를 담당한다. kube-system namespace 에 Deployment 와 관련 ConfigMap 으로 동작한다.

CoreDNS 의 질의 흐름은 아래와 같다.

  1. Pod 가 Service domain 으로 질의
  2. Pod 의 /etc/resolv.conf 가 CoreDNS ClusterIP 를 name server 로 가리킴
  3. CoreDNS 의 kubernetes 플로그인이 Service endpoint 를 조회해 응답
  4. Cluster domain 이 아니면 forward 플러그인이 upstream 으로 포워딩

ExternalDNS

Ingress, Service, Gateway 등 리소스를 감시해 R53, Cloud DNS 등 외부 DNS 제공자에 A, AAAA, CNAME 등의 레코드를 생성한다. 즉, 외부인터넷에서 kubernetes cluster 로 접근할 때 필요한 domain name 들을 외부 DNS 와 연동해주는 역할을 한다.

ExternalDNS 의 동기화 흐름은 아래와 같다.

  1. 컨트롤러가 Ingress, Service, Gateway 등 리소스를 감시
  2. 호스트명 또는 주석을 읽어 목표 레코드를 계산
  3. 소유권을 관리하는 TXT 레코드 확인
  4. DNS 제공자 API 로 A, AAAA, CNAME 등을 생성 및 갱신

Ingress


Ingress 란

쿠버네티스에서 외부 트래픽을 받기 위해선 NodePort 또는 LoadBalancer 타입의 Service 를 생성해야 한다. Service 가 많아질수록 각 Service 마다 LoadBalancer 를 만들면 비용이 증가하고, 여러 개의 NodePort 를 관리하기 복잡해지고, 도메인/경로 기반 라우팅이 불가능하다는 문제가 생긴다.

이를 해결하기 위해 Ingress 라는 리소스를 사용한다. Ingress 는 쿠버네티스 내부로 진입하는 단일 진입점 역할을 수행하며 다양한 Service 로의 트래픽을 구분하고 도메인 또는 경로별로 라우팅하는 일종의 Load Balancer 역할을 한다.

Ingress 는 도메인/경로 라우팅 규칙을 정의하는 Ingress 리소스와 실제로 트래픽을 처리하는 Ingress Controller 로 구성된다. Ingress Controller 의 구현체로는 Nginx, Traefik, HAProxy, Istio Ingress Gateway, ALB 등이 사용된다.

Service vs Ingress

Kubernetes 에서 Pod 는 동적으로 생성되고 사라지기에 IP 역시 자주 바뀐다. 때문에 외부 또는 클러스터 내에서 특정 Pod 에 안정적으로 접근하려면 고정된 네트워크 접근 방법이 필요하다. 이를 해결하기 위해 사용되는 것이 Service 이고 외부 HTTP/HTTPS 트래픽을 세밀하게 제어하기 위해 Ingress 가 사용된다. 즉, Service 는 내부/외부 통신을 연결하는 기본 통로, Ingress 는 HTTP/HTTPS 트래픽을 라우팅하는 고급 제어 도구다. Service 는 4계층(L4, TCP/UDP), Ingress 는 7계층(L7, HTTP/HTTPS)를 담당한다.

ServiceIngress
목적Pod 들을 묶어 네트워크 접근 지점 제공HTTP/HTTPS 요청을 경로 기반으로 라우팅
계층L4, TCP/UDPL7, HTTP/HTTPS
외부 노출필요 시 NodePort, LoadBalancer도메인/경로 기반 트래픽 제어
복잡도단순 연결라우팅, 인증, TLS 등 고급 제어
연관관계Ingress 는 내부적으로 Service 를 사용Service 없이는 Ingress 도 작동하지 않음

Service 는 여러 Pod 를 하나의 IP 로 접근 가능하게 해주는 기본 네트워크 객체이고, Ingress 는 HTTP 요청을 다양한 경로와 도메인으로 라우팅하는 고급 컨트롤러다. 즉, Ingress 는 Service 를 대체하는 것이 아닌 확장하는 상위 개념이다. Service 가 있어야 Ingress 가 트래픽을 어디로 보낼지 결정할 수 있다. 실무에선 Service + Ingress 조합으로 외부 트래픽을 내부 Pod 로 보낸다.

Ingress Controller

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-controller
spec:
  replicas: 1
  selector:
    matchLabels:
      name: nginx-ingress
  template:
    metadata:
      labels:
        name: nginx-ingress
    spec:
      serviceAccountName: ingress-serviceaccount
      containers:
      - name: nginx-ingress-controller
        image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0
        args:
        - /nginx-ingress-controller
        - --configmap=$(POD_NAMESPACE)/nginx-configuration
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        ports:
          - name: http
            containerPort: 80
          - name: https
            containerPort: 443

실제로 트래픽을 처리하는 Ingress Controller 의 역할을 수행할 구현체로 nginx-ingress 를 Deployment 로 배포한다.

apiVersion: v1
kind: Service
metadata:
  name: ingress
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    targetPort: 443
    protocol: TCP
    name: https
  selector:
    name: nginx-ingress

nginx-ingress Deployment Pod 로 외부에 노출하기 위해 NodePort 또는 LoadBalancer 타입의 Service 를 배포한다. 해당 Service 가 외부에서 쿠버네티스 클러스터로 접근하는 단일 진입점이 된다.

Ingress

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-wear-watch
spec:
  rules:
  - host: wear.my-online-store.com
    http:
      paths:
      - backend:
          serviceName: wear-service
          servicePort: 80
  - host: watch.my-online-store.com
    http:
      paths:
      - backend:
          serviceName: watch-service
          servicePort: 80

Ingress 가 여기서 사용된다.

Ingress TLS 설정

# openssl 로 tls.crt, tls.key 생성
openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout tls.key \
  -out tls.crt \
  -subj "/CN=myapp.example.com/0=myapp"
 
# Secret 으로 생성
k create secret tls my-tls-secret --cert=tls.crt --key=tls.key
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tls-ingress
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - myapp.example.com
    secretName: my-tls-secret # Ingress Controller 가 지정된 TLS 인증서 Secret 을 이용해 SSL Handshake 를 수행
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          serviceName: my-service
          servicePort: 80

Gateway API


apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass # 클러스터 전체에서 어떤 Gateway Controller 를 사용할 지 정하는 설계도
metadata:
  name: nginx
spec:
  controllerName: nginx.org/gateway-controller
--- # TLS/로드밸런서(IP, 인증서)는 운영자가 Gateway 에만 정의
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway # 어느 포트를 열고 어떤 도메인을 받을지 정의
metadata:
  name: example-gateway
spec:
  gatewayClassName: nginx # 위에 만든 GatewayClass
  listeners:
  - name: http
    protocol: HTTP
    port: 80
	allowedRoutes:
	  namespaces:
	    from: Same
  - name: https
    protocol: HTTPS
    port: 443
    tls:
	  mode: Terminate
	  certificateRefs:
	  - kind: Secret
	    name: tls-secret
	allowedRoutes:
	  namespaces:
	    from: All
--- # 개발자는 자기 Route 리소스만 관리 (서비스 라우팅)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute # 요청을 어느 서비스로 보낼지 정하는 규칙 'Route | HTTPRoute | TLSRoute | GRPCRoute'
metadata:
  name: app-route
spec:
  parentRefs:
  - name: example-gateway
  hostnames:
  - "www.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /login
    backendRefs:
    - name: example-svc
      port: 8080

Ingress 는 헤더 매칭, 리다이렉트, 트래픽 분할 등의 부족한 기능을 보완하기 위해 annotation 을 통해 Ingress Controller 의 설정을 활용한다. Ingress 는 기본적으로 HTTP/HTTPS 정도만 처리하기에 gRPC, TCP, UDP 같은 프로토콜을 지원하기 위해선 Ingress Controller 에 맞는 annotation 을 붙여 사용해야 한다. Ingress Controller 구현체 마다 annotation 이 달라서 Nginx Ingress Controller 를 사용하다가 Traefik Ingress Controller 를 사용하면 annotation 이 달라 설정을 다시 해줘야 한다. 즉, 표준이 아닌 Ingress Controller 별 종속적인 기능에 의존성이 생기는 문제가 있다. 뿐만 아니라 Ingress 는 트래픽 라우팅, TLS, 도메인, 리다이렉트 등 모든 설정이 하나의 Ingress 리소스 안에 섞여버리는 문제도 있다.

쿠버네티스에선 이를 해결하기 위해 Gateway API 라는 프로젝트를 시작했고, 제공되는 GatewayClass (인프라팀), Gateway (운영자), Route (개발자) 등 역할이 분리된 새로운 Object 를 통해 Controller 에 종속되지 않는 설정과 리소스 관리 충돌을 방지할 수 있다. Gateway API 는 쿠버네티스의 기본 리소스가 아니기 때문에 CRD 를 설치해야 한다.

Traefik GatewayClass 설치

helm repo add traefik https://traefik.github.io/charts # helm repo 에 traefik chart 를 추가하고
helm repo update                                       # helm repo update
helm install traefik traefik/traefik \
  -n traefik \                                         # traefik 이라는 namespace 에 설치
  --create-namespace \                                 # namespace 에 생성
  --set service.type=LoadBalancer \                    # traefik gateway controller 의 Service type
  --set providers.kubernetesGateway.enabled=true       # traefik 의 경우 Gateway API 를 활성화해줘야 한다.
# k get gatewayclass traefik -o yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: traefik
spec:
  controllerName: traefik.io/gateway-controller

쿠버네티스용 패키지 매니저 Helm 을 사용해서 Traefik GatewayClass 를 설치하자.

Gateway 생성

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: demo-gateway
  namespace: default
spec:
  gatewayClassName: traefik
  listeners:
  - name: http
    protocol: HTTP
    port: 8000 # 포트는 반드시 Traefik Deployment 에 정의된 EntryPoint 포트와 일치
	allowedRoutes:
	  namespaces:
	    from: All # Gateway 에 들어올 수 있는 namespace 설정 'All | Same | Selector'

HTTPRoute 생성

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: echo-route
  namespace: default
spec:
  parentRefs:
  - name: demo-gateway
  hostnames:
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: echo-server
      port: 80

References