LFS258 [7/15]: Kubernetes Services
Tags: lfs258, kubernetes, docker, devops
Published: 2020-03-15 06:54:10

# TL;DR

- [Services Overview](#services-overview): service เป็น object หนึ่งที่ทำหน้าที่เป็น load balancer แบบง่ายๆ ของ kubernetes จุดประงสงค์ คือ share load ระหว่าง pods ที่ทำงานแบบเดียวกับ โดยอาศัย Pods Label ในการระบุ Pods ที่เป็นตัวรับ load
- [Service Update Pattern](#service-update-pattern): ควรระบุ application version ใน label selector ของ service เพื่อเวลา update application ใน deployment จะได้ไม่มี traffic เข้าไปยัง pods ใหม่ก่อนที่จะพร้อม เมื่อทุกอย่างพร้อมค่อย Update label เพื่อชี้ไปยัง pods ใหม่
- [Accessing an Application with a Service](#accessing-an-application-with-a-service): เป็นการลองสร้าง service แบบ NodePort แล้วลอง เรียกใช้งาน server จากภายนอก
- [Service Types](#service-types): มี 4 ชนิด คือ ClusterIP, NodePort, LoadBalancer และ ExternalName
- [Services Diagram](#services-diagram): พูดถึง service แต่ละชนิด ได้แก่ user space proxy, iptables proxy, ipvs proxy
- [Local Proxy for Development](#local-proxy-for-development): เป็นการ run local proxy เชื่อเครื่องเรากับ cluster เพื่อใช้ในการ test service ที่เรายัง develope อยู่
- [DNS](#dns): Cluster DNS ของ Kuberbenetes ซึ่งตอนนี้ใช้ CoreDNS
- [Verifying DNS Registration](#verifying-dns-registration): การทำสอบ และ troubleshoot DNS services


<a name="Services-Overview"></a>

---

# Services Overview

จากที่เราทราบกันแล้วว่าหลักสำคัญของ kubernetes คือ "transient" และ "decoupled" นั่นหมายความว่า แต่ละ app ต้องแยกออกจากกันโดยอิสระ และ เกิดตายได้ตลอดเวลา รวมถึงการเพิ่มและลดจำนวน pod แบบอัตโนมัติ เพื่อให้เหมาะสมกับ traffic จึงเป็นเรื่องที่ยุ่งยากและซ้ำซ้อนมากหากเราจะต้องเขียน logic เอง เพื่องานนี้

kubernetes จึงมีสิ่งที่เรียกว่า "service" เพื่อมาช่วยเราในเรื่องนี้ service ทำหน้าที่เป็นคล้ายๆ load balancer แบบง่ายๆ ที่คอยตรวจสอบสถานะของ pod ที่มี label ตรงกับที่กำหนดไว้ เพื่อทำการ share load ไปยัง pod ต่างๆ ที่อยู่ในสภาวะพร้อมใช้งาน

โดยเบื้องหลัง service จะมี kube-proxy เป็นตัวช่วยตรวจสอบสถานะของ pod ที่มี label ตามที่กำหนดจาก kubernetes API และรวบรวม pods ที่อยู่ในสถานะพร้อมใช้งานเป็น `Endpoint` จากนั้นจึงสั่งให้ iptables พา traffic ไปยัง endpoint นั้นๆ โดย 1 service จะมี 1 IP คล้ายๆ กับ VIP ของ load balancer และ IP ดังกล่าวจะถูกเก็บไว้ที่ etcd database เพื่อให้ core dns (internal dns ของ kubernetes) นำไปใช้งาน ทำให้เราสามารถเรียกใช้งานด้วยชื่อของ service     

service สามารถชี้ไปยัง `Endpoint` ทั้งภายในและภายนอก cluster เช่น third party database

ใน Google มีการใช้พัฒนาและใช้งาน `EPS` (Extensible Service Proxy) ซึ่งมีพื้นฐานมาจาก nginx reverse proxy โดยมันมีความยืดหยุ่นและมีประสิทธิภาพมากกว่า `Endpoint` แต่ยังไม่ค่อยได้รับความสนใจจากภายนอกมากนัก

<a name="Service-Update-Pattern"></a>

---

# Service Update Pattern

จากที่เรารู้แล้วว่า `Service` จะแจก load ไปยัง pods ที่มี `Label` ตรงตามที่กำหนดไว้ และ `Label` นั้นสามารถ update ได้ตลอดเวลาโดยไม่มีผลกระทบอะไรกับ pods ทำให้เป็นไปได้ง่ายมากที่เราจะพลาด deploy application version ใหม่แล้ว มี traffic ไหลเข้าไปโดยทันที และเนื่องด้วย depfault update strategy ของ Kubernetes คือ "Rolling Update" ดังนั้น ระหว่างที่ update จะมี application version ใหม่ และ version เก่า run ไปพร้อมๆ กันอยู้ช่วงเวลาหนึ่ง ซึ่งถ้าอาจทำให้เกิดปัญหากับผู้เรียกใช้งานได้

วิธีที่แนะนำคือให้ระบุ version ของ application ลงไปใน `Label` ของ pods ใน `Deployment` และ ที่ `Service` ก็ระบุ `Label` ของ `Endpoint` ด้วย `Label` เดียวกันที่มี version ของ application ดังนั้น เมื่อทำการ update application version ให้ create `Deployment` ใหม่ขึ้นมา เมื่อเราตรวจสอบว่าทุกอย่างเรียบร้อยดี ค่อย update `Label` ของ `Endpoint` ของ `Service` ไปยัง version ใหม่ ตาม `Deployment` ใหม่ของเรา จากนั้นค่อย delete `Deployment` เก่าให้หมด

<a name="Accessing-an-Application-with-a-Service"></a>

---

# Accessing an Application with a Service

เราสามารถใช้ `kubectl expose ...` เพื่อทำการสร้าง `Services` ได้ โดยจะเป็นการสร้าง `Service` ประเภท `NodePort` ที่มีการ assign random port ขึ้นมาเพื่อรับ traffic จากภายนอก แล้ว map กับ port ของ pod ที่เป็น `Endpoint`

เราสามารถระบุ `port` และ `targetPort` ได้ด้วย ตอนสร้าง `Service` เพื่อลดความยุ่งยากหากเราต้องการเปลี่ยน port ของ Pods เราสามารถกำหนดชื่อของ port ใน deployment แล้ว ระบุ `port` ด้วย ชื่อที่เรากำหนดไว้ได้

range ของ random port นี้ สามารถกำหนดได้โดยระบุในตัวแปร `NodePort` ตอนสร้าง cluster

Service สามารถชี้ข้าม Namespace ได้ด้วย

ตัวอย่างการใช้งาน Service แบบง่ายๆ 

- สร้าง Pods และ Service ที่ชี้ไปยัง Pods นั้นๆ 

  ``` bash
  # kubectl create deployment nginx --image=nginx
  deployment.apps/nginx created

  # kubectl get deployment   
  NAME    READY   UP-TO-DATE   AVAILABLE   AGE
  nginx   0/1     1            0           9s

  # kubectl expose deployment/nginx --port=80 --type=NodePort
  service/nginx exposed
  ```

- ตรวจสอบความสัมพันธ์

  ``` bash
  # kubectl get pod
  NAME                     READY   STATUS    RESTARTS   AGE
  nginx-86c57db685-qrkm2   1/1     Running   0          48m

  # kubectl get svc
  NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
  kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        58d
  nginx        NodePort    10.110.137.109   <none>        80:32285/TCP   18s  #<-- Service ที่เราสร้างขึ้น

  # kubectl get pod nginx-86c57db685-qrkm2 -o yaml
  apiVersion: v1
  kind: Pod
  metadata:
    annotations:
      cni.projectcalico.org/podIP: 192.168.32.49/32
      cni.projectcalico.org/podIPs: 192.168.32.49/32
    creationTimestamp: "2020-03-14T22:39:35Z"
    generateName: nginx-86c57db685-
    labels:
      app: nginx                      #<-- label ของ Pod
      pod-template-hash: 86c57db685
    name: nginx-86c57db685-qrkm2
    namespace: default
    ownerReferences:
          :           :

  # kubectl get svc nginx -o yaml
  apiVersion: v1
  kind: Service
  metadata:
    creationTimestamp: "2020-03-14T22:39:48Z"
    labels:
      app: nginx
    name: nginx
    namespace: default
    resourceVersion: "12379326"
    selfLink: /api/v1/namespaces/default/services/nginx
    uid: 31b30266-157c-4ffb-8830-52613f712eaa
  spec:
    clusterIP: 10.110.137.109
    externalTrafficPolicy: Cluster
    ports:
    - nodePort: 32285                 #<-- Random port ที่ถูก assign เพื่อใช้รับ traffic จากภายนอก
      port: 80                        #<-- Port ของ Pod ที่เป็น Endpoint
      protocol: TCP
      targetPort: 80                  #<-- ตาม default จะเป็นค่าเดียวกับ port:
    selector:
      app: nginx                      #<-- label ของ Pod ที่ Service ชี้ไป 
    sessionAffinity: None
    type: NodePort
  status:
    loadBalancer: {}
  ```

- ลอง curl ไปยัง IP ของ server ใดก็ได้ ใน cluster จะเห็นว่าสามารถทำงานได้

  ``` bash
  # curl 127.0.0.1:32285
  <!DOCTYPE html>
  <html>
  <head>
  <title>Welcome to nginx!</title>
  <style>
      body {
          width: 35em;
          margin: 0 auto;
          font-family: Tahoma, Verdana, Arial, sans-serif;
      }
  </style>
  </head>
  <body>
  <h1>Welcome to nginx!</h1>
  <p>If you see this page, the nginx web server is successfully installed and
  working. Further configuration is required.</p>

  <p>For online documentation and support please refer to
  <a href="http://nginx.org/">nginx.org</a>.<br/>
  Commercial support is available at
  <a href="http://nginx.com/">nginx.com</a>.</p>

  <p><em>Thank you for using nginx.</em></p>
  </body>
  </html>
  ```

<a name="Service-Types"></a>

---

# Service Types

`Service` ของ kubernetes มี 4 ประเภท คือ

- **ClusterIP**: `เป็น service ที่สามารถใช้ภายใน cluster เท่านั้น เป็น default type`
- **NodePort**: `เป็น service ที่จะ listen random port ที่กำหนดไว้ที่ทุกๆ server ใน cluster เราสามารถกำหนด range ของ port โดยระบุในตัวแปร NodePort หรือ option --service-node-port-range ตอนสร้าง cluster (default = 30000-32767) ในการใช้งานต้องมี static IP และต้อง allow Firewall ให้ครบทุก servers`
- **LoadBalancer**: `เป็นการ expose service ให้ภายนอกใช้ ผ่าน load balancer ของ Cloud Providers โดยจะต้องมีการระบุ Cloud Providers ตอน สร้าง cluster เพื่อให้ kubernertes ไปคุยกับ API ของ Cloud Provider ได้ นั่นหมายความว่า จะต้องเป็น Cloud Provider ที่ Support ด้วย`
- **ExternalName**: `ไม่มีการกำหนด selector โดยจะเป็นการ map service กับ External Domain name โดยใช้ CNAME ของ cluster DNS` 

<a name="Services-Diagram"></a>

---

# Services Diagram

การทำงานของ `Service` ที่ไม่ใช่ `ExternameName` จะมี `kube-proxy` เป็นคนคอยจัดการตรวจสอบสถานะของ resources จาก API server และจัดการเรื่อง virtual server ให้ โดย technic ที่ `kube-proxy` ใช้มีการเปลี่ยนแปลงไปเรื่อยๆ ในแต่ละ version

ใน v1.0, `Service` จะทำงานใน mode **user space proxy** คือ จะทำงานเป็น Layer 4 proxy ใน mode นี้สามารถทำ SessionAffinity ได้ และ default algoritm คือ round-robin เบื้องหลังการทำงานก็คือ "iptables rules" นั่นเอง

![user-space-proxy](https://d33wubrfki0l68.cloudfront.net/e351b830334b8622a700a8da6568cb081c464a9b/13020/images/docs/services-userspace-overview.svg)

ใน v1.1, mode **iptables proxy** ถูกเพิ่มเข้ามาและกลายเป็น default mode ใน v1.2 เป็นต้นไป โดย ใน mode นี้ยังคงทำงานโดยอาศัย iptable rules เหมือนเดิม แต่เปลี่ยนมาใช้ "Linux Nerfilter" แทน user space ทำให้ไม่ต้อง switch ไปมาระหว่าง user space และ kernel space เป็นผลให้ทำงานได้ขึ้น 

นอกจากนั้นใน mode นี้ยังเพิ่มการ retry ไปยัง pod อื่น ให้อัตโนมัติ หาก pod ส่ง request ไปไม่ตอบสนอง 

และเพื่อลดการ retry ใน mode นี้ ยังมีการตรวจสอบ status ของ readiness probe ของ pods ด้วย โดยจะส่ง request ไปยัง pods ที่ readiness status มีค่าเป็น healthy เท่านั้น

![iptables-proxy](https://d33wubrfki0l68.cloudfront.net/27b2978647a8d7bdc2a96b213f0c0d3242ef9ce0/e8c9b/images/docs/services-iptables-overview.svg)

ใน v1.9, mode **IPVS proxy** ถูกเพิ่มในเข้ามา และ stable ใน v1.11 โดยใน mode นี้เป็นจะการแก้ปัญหาเรื่อง overhead ของ iptables rule ใน cluster ที่ใหญ่มากๆ (5000 nodes ขึ้นไป) เพราะ iptable ทำงานแบบ rule-base ที่ check จากบนลงล่าง ดังนั้นเมื่อ rule เยอะขึ้น overhead ก็มากขึ้น kubernetes จึงเปลี่ยนมาใช้ IPVS ของ Linux ในการสร้าง virtual server และ share load ไปยัง Endpoint 

ด้วย feature ของ IPVS ทำให้ ใน mode นี้ share load ได้หลาย algorithm เช่น `rr`: round-robin, `lc`: least connection, `dh`: destination hashing, `sh`: source hashing, `sed`: shortest expected delay และ `nq`: never queue

![ipvs-proxy](https://d33wubrfki0l68.cloudfront.net/2d3d2b521cf7f9ff83238218dac1c019c270b1ed/9ac5c/images/docs/services-ipvs-overview.svg)

<a name="Local-Proxy-for-Development"></a>

---

# Local Proxy for Development

จากในการทำงาน หากจะ test service ที่เรากำลัง develope อยู่ ตัวช่วยที่ง่ายที่สุดคือ **local proxy** 

Local proxy จะสร้าง encryption tunnel ระหว่างเครื่องเราและ cluster โดยทำให้เราสามารถเรียก localhost ของเราเพื่อต่อไปยัง Kubernetes API server ของ cluster เราได้ 

จากนั้นเราสามารถ curl เข้าไปยัง `Service` ของเราได้ โดยใช้ URL ดังนี้

> `http://localhost:8001/api/v1/namespaces/<namespace_name>/services/<service_name>:<port>/proxy/`

``` bash
# kubectl proxy &
Starting to serve on 127.0.0.1:8001

# kubectl get svc
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        58d
nginx        NodePort    10.110.137.109   <none>        80:32285/TCP   6h37m

# curl  http://localhost:8001/api/v1/namespaces/default/services/nginx:80/proxy/  
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

<a name="DNS"></a>

---

# DNS

ตั้งแต่ v1.13, Cluster DNS ที่ kubernetes ใช้โดย default คือ **CodeDNS** ซึ่งสามารถเพิ่ม functionality ได้โดยการเพิ่ม plugin 

เราสามารเข้าไปเลือก plugin ที่เราต้องการได้จาก [CoreDNS Plugins](https://coredns.io/plugins/) โดย Plugins ที่ใช้บ่อยๆ ตัวอย่างเช่น การ export metrics ให้ prometheus, error logging, health reporting และ TLS เพื่อ configure certificate และ gRPC servers

<a name="Verifying-DNS-Registration"></a>

---

# Verifying DNS Registration

เมื่อเราทำการ set up cluster แล้ว จะรู้ได้อย่างไรว่า CoreDNS ใช้งานได้ ? วิธีการง่ายๆ ก็คือ run pod ขึ้นมา แล้วลง nslookup จากในนั้น


``` bash
# kubectl get svc
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        58d
nginx        NodePort    10.110.137.109   <none>        80:32285/TCP   7h5m

# kubectl run --generator=run-pod/v1 -it --rm busybox --image=busybox
If you don't see a command prompt, try pressing enter.
/ # cat /etc/resolv.conf            #<-- check that DNS server is properly configure
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local openstacklocal novalocal
options ndots:5

/ # nslookup nginx                  #<-- Test nslookup for the service nginx
Server:         10.96.0.10
Address:        10.96.0.10:53

Name:   nginx.default.svc.cluster.local
Address: 10.110.137.109

/ # exit
```

ถ้าพบปัญหา อาจลองดู Log ของ pods ที่มีชื่อขึ้นต้นด้วย coredns โดยหาบรรทัดที่มีคำว่า W (warning), E (error) และ F (failure)