LFS258 [4/15]: Kubernetes APIs and Access
Tags: lfs258, kubernetes, docker, devops
Published: 2020-02-03 00:47:57

จากบทที่แล้วเราได้รู้แล้วว่า APIs เป็นหัวใจสำคัญของ Kubernetes การทำงานทุกอย่างต้องถูกสั่งด้วยการเรียก API ผ่าน `kube-apiserver` ที่อยู่บน master node ในบทนี้เราจะมาเจาะลึกเรื่อง APIs และ การเรียกใช้มันดูกัน

# TL;DR

- [API Access](#api-access) APIs ของ Kubernetes เป็น REST-based API การเข้าถึงสามารถใช้ `curl` หรือ tools อื่นๆ ก็ได้ แต่ต้องมี cert และ key ที่ถูกต้อง และ สิทธิ์ ที่เหมาะสม
- [Checking Access Privilege](#check-access) สามารถทำได้โดยใช้ command `kubectl auth can-i ...`
- [Optimistic Concurrency](#optimistic-concurrency) การ update resources ของ kubernetes จะไม่ใช้การ locking แต่ใช้การอ้างอิง version ของ resource และ retry หาก ได้รับ error `409 CONFLICT` เนื่องจาก version ของ resource ไม่ตรงกับ version ปัจจุบัน
- [Using Annotations](#using-annotations) annotation เป็นที่เก็บ metadata ให้กับ object ไม่เหมือนกับ labels ที่ใช้สำหรับการ identify resources ที่ต้องการสั่งงาน
- [Simple Pod](#simple-pod) อาจมีเพียง 1 container หรือมากว่านั้นก็ได้ แต่โดยปกติแล้วจะมี 1 container ที่ทำ logic หลัก นอกนั้นจะทำหน้าที่เป็นตัวคอยช่วยเหลือ เรียกว่า sidecar
- [What kubectl really do](#kubectl) `kubectl` จะทำหน้าที่แปลงคำสั่ง ที่เราสั่งผ่าน kubectl ให้กลายเป็น REST-based API call พร้อมทั้งแปลง configuration ที่เป็น YAML ให้เป็น JSON format เพื่อส่งไปยัง kube-apiserver แทนเรา
- [Access from Outside the Cluster](#access-from-outside) ในการเข้าถึง API นั้น cert และ key ต้องถูกต้อง ในกรณีข้อง `kubectl` จะอ่าน cert และ key จาก `~/.kube/config`
- [Namespace](#namespace) ของ kubenetes จะหมายถึงการแบ่ง (segmentation) object ภายใน cluster ออกเป็นกลุ่มๆ ไม่ใช่ kernel เหมือนของ Linux
- [Working with Namespaces](#working-with-namespace) เราสามารถสร้าง namespace ขึ้นมาใช้งานเองได้ และ assign resources ไปยัง namespace ที่เราสร้างขึ้นมาใหม่ได้เลย
- [API Resources of Kubernetes](#api-resources) สามารถเข้าถึงได้ทั้ง `kubectl` และ REST API call
- [API Maturity](#api-maturity) Kubernetes มีการแบ่ง API ออกเป็นกลุ่มๆ และภายในกลุ่มยังแบ่งออกเป็น version ทำให้แต่ละ team สามารถแยกพัฒนา API กันได้อย่างอิสระ ในการ release version มี 3 stages คือ `alpha`, `beta` และ `stable`

<a name="api-access"></a>

---

# API Access

![arch](https://thepracticaldev.s3.amazonaws.com/i/bm469cdmaodmlk11smcw.png)

APIs ของ Kubernetes เป็น REST-based API และด้วยความที่ kubernetes เติมโตขึ้นเรื่อยๆ ตลอดเวลา ดังนั้น ความเข้าใจเรื่อง endpoint และ การเปลี่ยนแปลง API specification ในแต่ละ version เป็นเรื่องที่สำคัญและต้องติดตาม โดยตั้งแต่ v1.16 เป็นต้นมา object ที่ไม่ได้ไปต่อ (deprecated) จะเรียกใช้งานไม่ได้แล้ว 

เนื่องจาก Kubernetes ใช้ REST-based API ดังนั้นเราสามารถ `curl` เข้าไปเรียกใช้งาน APIs ได้เลย โดย API ของ Kubernetes จะแบ่งออกเป็นกลุ่มๆ ในแต่ละกลุ่มจะมีการพัฒนาแยกกับกลุ่มอื่นโดยอิสระ ภายในกลุ่มก็ยังแบ่งเป็น version ในการเรียกใช้ ต้องระบุกลุ่ม และ version ให้ชัดเจน

ในการเรียกใช้งาน API เราสามารถใช้ HTTP verbs เช่น `GET`, `POST` หรือ `DELETE` ได้ตามปกติ โดยสามารถใช้ `curl` หรือ tool อื่นๆ ก็ได้ แต่สิ่งสำคัญที่สุดที่ต้องมี คือ **certificates** และ **keys** ที่ถูกต้องเหมาะสม เราสามารถระบุรายละเอียดของคำสั่งใน JSON file แล้วส่งมันไปพร้อมกับ HTTP request เพื่อสั่งงาน kubernetes ได้ด้วย

``` bash
$ curl \
--cert userbob.pem \
--key userbob-key.pem \
--cacert /path/to/ca.pem \
--http://k8sMaster:6443/api/v1/pods
```

ปกติแล้วเราจะไม่ได้ curl เข้าไปตรงๆ แต่จะใช้ command `kubectl` ในการ call ไปยัง `kube-apiserver` โดยเราสามารถเขียน configuration file ใน YAML format ส่งให้ `kubectl` และ `kubectl` จะทำหน้าที่แปลง YAML ให้เป็น JSON จากนั้นจึงส่งให้ `kube-apiserver` อีกที

นอกจากนั้น เรายังสามารถสั่งงานในนามของ user หรือ group อื่นได้ (impersonate) แต่เราต้อง configure [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) ก่อน

<a name="check-access"></a>

---

# Checking Access Privilege

ก่อนจะเข้าไปงงด้วยกันในเรื่อง security ใน chapter หลังๆ kubernetes มี sub-command ที่จะช่วยให้เราตรวจสอบ authorization ทั้งของเราเองในฐานะ admin และ user อื่นๆ ได้ด้วย นั่นก็คือ `kubectl auth can-i ...` ดังตัวอย่างดังต่อไปนี้

``` bash
# Can I, as an administrator, create deployment in default namespace?
$ kubectl auth can-i create deployments
yes

# Can I as, a bob, create deployment in default namespace?
$ kubectl auth can-i create deployments --as bob
no

# Can I as, a bob, create deployment in developer namespace?
$ kubectl auth can-i create deployments --as bob --namespace developer
yes
```

โดย sub-command นี้ไปเรียกใช้ `kind` ดังต่อไปนี้

- **`SubjectAccessReview`**: เป็นการตรวจสอบความสามารถของ user ใดๆ ก็ได้
- **`LocalSubjectAccessReview`**: เหมือน `SubjectAccessReview` แต่เป็นการตรวจสอบเฉพาะ namespace หนึ่งๆ เท่านั้น
- **`SelfSubjectRulesReview`**: เป็นการตรวจสอบว่า user นั้นๆ สามารถทำอะไรบ้างใน namespace นั้นๆ

ของ API Group `authorization.k8s.io/v1` 

นอกจากนี้ยังมี [`kubectl auth reconcile`](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#-em-reconcile-em-) ที่ช่วย reconcile rules ของ RBAC Role, RoleBinding, ClusterRole และ ClusterRole binding objects ให้เป็นไปตาม configure ของเรา ถ้ามีอะไรหายไปมันก็จะแก้ไขให้ถูกต้อง

<a name="optimistic-concurrency"></a>

---

# Optimistic Concurrency

โดย default data ที่จะส่งให้ API จะถูก [serialization](https://en.wikipedia.org/wiki/Serialization) ให้ในรูป JSON ต่อมา Google ได้เสนออีกวิธีในการ serialization เรียกว่า [protobuf](https://developers.google.com/protocol-buffers) ซึ่งยังอยู่ในขั้นทดลอง ในทางปฏิบัติ เราจะใช้ YAML format ซึ่ง `kubectl` จะเป็นคนแปลงให้เป็น JSON เพื่อส่งไปยัง `kube-apiserver` ต่อไป

ถ้าใครเคยผ่านเรื่อง database มาแล้ว น่าจะเข้าใจ concept ของการ locking คือ ถ้า นาย A กำลัง update row ที่ 3 อยู่ แล้วนาย B ต้องการ update row 3 เหมือนกัน นาย B ต้องรอให้ นาย A update เสร็จก่อน ถึงจะ update ได้

แต่ kubernetes ไม่ได้เป็นแบบนั้น มันจะใช้ [`versionResource`](https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions) บวกกับ [`optimistic concurrency`](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) มาจัดการเรื่องนี้แทน โดย ทุกการ update ต้อง ระบุ version ของ resource ที่จะ update ด้วยเสมอ ถ้า version ไม่ตรง (อาจมีคนอื่นมาชิง update ก่อน) จะได้รับ error `409 CONFLICT` คนนั้นต้องกลับไป check version ของ resource นั้นก่อน แล้วค่อยมา update ใหม่เองอีกครั้ง ทั้งนี้ operation ที่ไม่ทำให้ resource เกินการเปลี่ยนแปลง เช่น `WATCH` หรือ `GET` จะไม่ทำให้ `versionResource` เปลี่ยนแปลง

kubernetes ได้ `versionResource` มาจาก `modifiedIndex` ของ **etcd database** ซึ่งมันจะไม่ซ้ำกันใน `namespace`, `kind` และ `server` เดียวกัน

<a name="using-annotations"></a>

---

# Using Annotations

Annotations คล้ายกับ labels ตรงที่ใช้เพื่อกำหนด metadata ให้กับ object แต่ annotations ไม่ได้มีไว้ใช้ในการระบุ หรือ เลือก resources เพื่อมาทำงานอย่างใดอย่างหนึ่งเหมือน labels มันทำหน้าที่แค่เก็บ metadata เพื่อเป็นข้อมูลให้คนอื่นมาอ่าน มันจึงมีความยืดหยุ่นในการเก็บข้อมูลมากกว่า

ตัวอย่างของ data ที่เก็บใน metadata เช่น
 
- Build, release หรือ image information
- Pointers ของ logging, monitoring, analytics หรือ audit repositories
- Client library หรือ tool information
- Contact Point

เป็นต้น 

annotations สามารถเก็บไว้ใน external database ได้ แต่จะทำให้ความยืดหยุ่นลดลงไม่ควรทำ

ตัวอย่างการกำหนด annotation

``` bash
# define annotation
$ kubectl annotate pods --all description='prod' -n prod

# overwrite annotation
$ kubectl annotate --overwrite pods description='old-prod' -n prod

# remove annotation
$ kubectl annotate pods foo description- -n prod
```

<a name="simple-pod"></a>

---

# Simple Pod

จาก chapter ที่แล้ว Pod คือ หน่วยที่เล็กที่สุดที่ Kubernetes สามารถควบคุมได้ ใน pod อาจมีเพียง 1 container หรือมากว่านั้นก็ได้ แต่โดยปกติแล้วจะมี 1 container ที่ทำ logic ของ app นอกนั้นจะทำหน้าที่เป็นตัวคอยช่วยเหลือ เรียกว่า sidecar

ในการสร้าง pod สามารถ define ด้วย YAML file ดังนี้

``` yaml
apiVersion: v1
kind: Pod
metadata:
  name: firstpod
spec:
  containers:
  - image: nginx
    name: firstpod
```

จากนั้นทำการ create ด้วย `kubectl create` เราสามารถตรวจสอบ status ของ pod ด้วย `kubectl get pods` ดังตัวอย่าง

``` bash
# Create pod which define in simple.yaml
$ kubectl crate -f simple.yaml

# Get all pods status in default namespace
$ kubectl get pods

# Get all pods status name firstpod in default namespace in YAML format
$ kubeclt get pod firstpod -o yaml

# Get all pods status name firstpod in default namespace in JSON format
$ kubeclt get pod firstpod -o json
```

<a name="kubectl"></a>

---

# What kubectl really do

kube-apiserver เป็นศูนย์กลางในการรับ REST-based API เพื่อให้เราสามารถสั่งงาน cluster ของเราได้ โดย เราสามารถ curl ไปยัง URL ที่เป็น endpoint ด้วย HTTP verbs มาตรฐาน (GET, PUT, POST, DELETE, ...) และอาจมี data ในรูปแบบ JSON ส่งไปด้วย เพื่อระบุรายละเอียด 

kubectl ก็ทำงานเช่นเดียว curl โดย kubectl จะทำหน้าที่แปลงคำสั่ง ที่เราสั่งผ่าน kubectl ให้กลายเป็น REST-based API call เพื่อส่งไปยัง kube-apiserver แทนเราอีกที

เราสามารถ debug การทำงานของ kubectl ได้โดยการใส่ option `--v=n` โดยที่ n คือ 0-9 ไปให้มัน ดังนี้

``` bash
$ kubectl --v=7 get pods 
I0201 08:11:43.476150   22800 loader.go:375] Config loaded from file:  /root/.kube/config
I0201 08:11:43.486369   22800 round_trippers.go:420] GET https://k8smaster:6443/api/v1/namespaces/default/pods?limit=500
I0201 08:11:43.486646   22800 round_trippers.go:427] Request Headers:
I0201 08:11:43.486835   22800 round_trippers.go:431]     Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io, application/json
I0201 08:11:43.487011   22800 round_trippers.go:431]     User-Agent: kubectl/v1.16.1 (linux/amd64) kubernetes/d647ddb
I0201 08:11:43.502660   22800 round_trippers.go:446] Response Status: 200 OK in 15 milliseconds
NAME                         READY   STATUS    RESTARTS   AGE
secondapp-5cf87c9f48-q8fxj   1/1     Running   0          5d1h
thirdpage-579947d95-mx2tp    1/1     Running   0          5d

$ kubectl --v=7 delete pod thirdpage-579947d95-mx2tp
I0201 08:12:32.998052   23824 loader.go:375] Config loaded from file:  /root/.kube/config
I0201 08:12:33.008824   23824 round_trippers.go:420] DELETE https://k8smaster:6443/api/v1/namespaces/default/pods/thirdpage-579947d95-mx2tp
I0201 08:12:33.008858   23824 round_trippers.go:427] Request Headers:
I0201 08:12:33.008868   23824 round_trippers.go:431]     Accept: application/json
I0201 08:12:33.008877   23824 round_trippers.go:431]     Content-Type: application/json
I0201 08:12:33.008907   23824 round_trippers.go:431]     User-Agent: kubectl/v1.16.1 (linux/amd64) kubernetes/d647ddb
I0201 08:12:33.043031   23824 round_trippers.go:446] Response Status: 200 OK in 34 milliseconds
pod "thirdpage-579947d95-mx2tp" deleted
I0201 08:12:33.045222   23824 round_trippers.go:420] GET https://k8smaster:6443/api/v1/namespaces/default/pods?fieldSelector=metadata.name%3Dthirdpage-579947d95-mx2tp
I0201 08:12:33.045239   23824 round_trippers.go:427] Request Headers:
I0201 08:12:33.045247   23824 round_trippers.go:431]     Accept: application/json
I0201 08:12:33.045257   23824 round_trippers.go:431]     User-Agent: kubectl/v1.16.1 (linux/amd64) kubernetes/d647ddb
I0201 08:12:33.053650   23824 round_trippers.go:446] Response Status: 200 OK in 8 milliseconds
I0201 08:12:33.054186   23824 round_trippers.go:420] GET https://k8smaster:6443/api/v1/namespaces/default/pods?fieldSelector=metadata.name%3Dthirdpage-579947d95-mx2tp&resourceVersion=2236351&watch=true
I0201 08:12:33.054201   23824 round_trippers.go:427] Request Headers:
I0201 08:12:33.054208   23824 round_trippers.go:431]     User-Agent: kubectl/v1.16.1 (linux/amd64) kubernetes/d647ddb
I0201 08:12:33.054216   23824 round_trippers.go:431]     Accept: application/json
I0201 08:12:33.056778   23824 round_trippers.go:446] Response Status: 200 OK in 2 milliseconds
```

<a name="access-from-outside"></a>

---

# Access from Outside the Cluster

เรารู้แล้วว่า `kubectl` จะช่วยให้ชีวิตของเราง่ายขึ้นด้วยการแปลง command เป็น HTTP request แล้วส่งไปยัง kube-apiserver ให้เรา และก่อนหน้านี้เราก็รู้แล้วด้วยว่า การ request เข้าไปยัง kube-apiserver นั้น จะทำได้ก็ต้องมี certification และ key ที่ถูกต้องด้วย 

คำถามคือ `kubectl` ได้อย่างๆรว่า certificate และ key ที่ถูกต้องคืออะไร ?

ถ้าเราย้อนกลับไปดูหัวข้อก่อนหน้านี้ ที่กล่าวถึงเรื่องการ debug `kubectl` จะเห็นว่าบรรทัดแรกของการ debug มีการอ่านค่าจาก file ชื่อ `/root/.kube/config` นั่นแหละครับ มันคือ configuration file ของ `kubectl` ที่เก็บ certificate และ key ที่ถูกต้องไว้ให้

โดย default, `kubectl` จะอ่าน configuration file จาก home directory ของ user นั้นๆ (`~/.kube/config`)

ตัวอย่าง `~/.kube/config` เป็นดังนี้

``` yaml
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0tLS1CRUdJTi...
    server: https://k8smaster:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
  user:
    client-certificate-data: LS0tLS1CRUdJTi...
    client-key-data: LS0tLS1CRUdJTi...
```

จาก file ด้านบน แต่ละตัวแปรมีความหมายดังนี้

- **apiVersion**: `ใช้กำหนด version ของ API ของ kube-apiserver ที่เราจะคุยด้วย`
- **clusters**: `ใช้กำหนด list ของ cluster ซึ่งประกอบด้วย name: ที่ใช้อ้างอิง และ`
  - **certificate-authority-data**: `CA ที่ใช้ในการ authentication request`
  - **server**: `URL ของ kube-apiserver`
- **users**: `ใช้กำหนด list ของ user ซึ่งประกอบด้วย name: ที่ใช้อ้างอิง และ`
  - **client-certificate-data**: `certificate`
  - **client-key-data**: `key`
- **contexts**: `ใช้กำหนด list ของ context ซึ่งประกอบด้วย name: ที่ใช้อ้างอิง และ`
  - **user**: `อ้างอิงมากจาก name: ของ users:`
  - **cluster**: `อ้างอิงมากจาก name: ของ clusters:`
- **current-context**: `เป็นการระบุว่า command ที่เราส่งไปนั่น จะถูกส่งไปด้วย context ไหน โดยอ้างอิงจาก contexts: อีกที`
- **kind**: `ใช้กำหนดชนิดของ object นี้ นั้นก็คือ Config เสมอ`
- **preference**: `เป็น optional ยังไม่ใช้ในตอนนี้`

<a name="namespace"></a>

---

# Namespace

คำนี้ถ้าใครเคยอ่าน บนความก่อนๆ ของผมเรื่อง [Containers Fundametals](https://dev.to/peepeepopapapeepeepo/lfs253-containers-fundamentals-1595) น่าจะรู้จัก namespace ในระดับ kernel เป็นอย่างดี และ kubernetes ก็มี namespace เช่นกัน 

โดยทั้งสองมีจุดมุ่งหมายที่เหมือนกันคือ เพื่อแบ่งแยก resource แต่ kubenetes จะหมายถึงการแบ่ง (segmentation) object ภายใน cluster ออกเป็นกลุ่มๆ ไม่ใช่ kernel

ในการ call API ของ kubernetes ต้องมีการระบุ namespace ด้วยเสมอ แต่ถ้าเราสั่งงานด้วย `kubectl` แล้วไม่ระบุ namespace `kubectl` จะทำงานกับ namespace ที่ชื่อว่า default โดยอัตโนมัติ

namespace ที่มาพร้อมกับการติดตั้ง cluster มีดังนี้
- **default**: `เป็นที่อยู่ของ resources ที่ไม่ได้ระบุ namespace`
- **kube-node-lease**: `เป็น namespace ที่เก็บ lease information`
- **kube-public**: `เป็น namespace ที่เก็บ information ทั่วๆ ไปที่ทุกคนอ่านได้ แม้แต่คนที่ไม่ได้ authenticate ก็ตาม`
- **kube-system**: `เป็นที่เก็บ infrastructure pods เช่น kube-apiserver, CoreDNS, kube-controller-manager, kube-scheduler, etc`

เราสามารถตรวจสอบสถานะของ resources ที่อยู่ในทุก namespace ด้วยการใส่ option `--all-namespaces` ให้กับ `kubectl`

<a name="working-with-namespace"></a>

---

# Working with Namespaces

``` bash
# List all namespaces in this cluster
$ kubectl get ns

# Create namespace called my_namespace
$ kubectl crete ns my_namespace

# Show detail of namespace name my_namespace
$ kubectl describe ns my_namespace

# Get status of namespace name my_namespace in YAML format
$ kubectl get ns/my_namespace -o yaml

# Delete namespace name my_namespace
$ kubectl delete ns/my_namespace
```

เมื่อ create namespace เรียบร้อยแล้ว ก็จะสามารถ assign pod ไปยัง namespace ได้ด้วยการระบุใน YAML file ดังนี้

``` yaml
apiVersion: v1
kind: pod
metadata:
  name: redis
  namespace: my_namespace  #<-- Here
    :     :
```

<a name="api-resources"></a>

---

# API Resources of Kubernetes

## kubectl

syntax ในการใช้ command `kubectl` เป็นดังนี้

> `kubectl [command] [type] [name] [flag]`

โดย 
- **command**: `verbs ที่เราจะทำกับ resource` ([`list of commands`](https://kubernetes.io/docs/reference/kubectl/overview/#operations))
- **type**: `type ของ resources ที่เราจะจัดการกับมัน` ([`list of resource types`](https://kubernetes.io/docs/reference/kubectl/overview/#resource-types))
- **name**: `ชื่อของ resources นั้นๆ`
- **flag**: `option เสริมระบุรายละเอียดของของคำสั่ง`

## REST API

หากต้องการดู log ของ pods ปกติจะใช้ `kubectl logs firstpod` แต่ถ้าต้องการส่งเป็น `curl` ไปแทนต้องส่งไปดังนี้ 

``` bash
$ curl \
--cert /tmp/client.pem \
--key /tmp/client-key.pem \
--cacert /tmp/ca.pem \
-v \
-XGET \
https://k8sMaster:6443/api/v1/namespaces/default/pods/firstpod/log
```

โดยยังมี endpoint อื่นๆ ที่สามารถ call ได้ดังตัวอย่างดังต่อไปนี้

``` txt
GET /api/v1/namespaces/{namespace}/pods/{name}/exec
GET /api/v1/namespaces/{namespace}/pods/{name}/log
GET /api/v1/watch/namespaces/{namespace}/pods/{name}
```

> API ทั้งหมดของ Kubernetes ออกแบบโดยใช้ Swagger ซึ่งเป็นไปตาม [OpenAPI](https://swagger.io/specification/)

<a name="api-maturity"></a>

---

# API Maturity

API ของ Kubernetes มีการแบ่งออกเป็นกลุ่มๆ และภายในกลุ่มยังแบ่งออกเป็น version ทำให้แต่ละ team สามารถแยกพัฒนา API กันได้อย่างอิสระ โดย release ของ API version มี 3 แบบ

- **Alpha Release**: `โดย default จะ disable ไว้เพราะยังมี bug อยู่ และอาจมีการเปลี่ยนแปลงได้ตลอดเวลา รวมทั้งสามารถยกเลิกการใช้งานได้ตลอด ตัวอย่างเช่น v1alpha1`
- **Beta Release**: `โดย default จะ enable ไว้ release นี้ ได้รับการ Test มาดีในระดับหนึ่ง รองรับ backward compatibility อาจยังมี bugs และ issues อยู่ ตัวอย่างเช่น v2beta3`
- **Stable Release**: `stable แล้ว ได้รับการ test มาเป็นอย่างดี มีโอกาสพบ bug บ้าง แต่ไม่มาก ตัวอย่างเช่น v1`