## Python Restful API
把Flask想像成一個小Server(或稱為一個框架),我們可以透過Flask將我們寫好的Python Restful API起在上面
(參考以下網址:https://www.analyticsvidhya.com/blog/2022/01/rest-api-with-python-and-flask/#What_Is_Flask-Restful?)

只使用Flask來當作Server,實在是很不靠譜,很容易就掛了,這樣就必須一直重啟Service...,所以加點東西吧(來點WSGI)
(參考網址:https://www.minglunwu.com/notes/2021/flask_plus_wsgi.html)
WSGI是一個協定,它簡單來說就是Http Request與我們API的中繼站。基於這個協定,我們來建立一個WSGI server(目前常見的是gunicorn & uwsgi)
(其實Flask裡面是有內建一個叫做Werkzeug的WSGI server啦,但他有點weak,遇到大量request會承擔不了。)


接著,我們加個Web server在前面吧(ex:Apache/Nginx...)

Web server扮演以下幾個角色:
1. 靜態檔案快取(加快response速度)
2. Load Balance(自動分流)
3. 隱藏真正Server位置(對於Client端都只知道他們打到了Web Server)
好,現在出現了另一個框架,我們該把框架從Flask改成fastAPI了
(參考網址:https://www.minglunwu.com/notes/2021/fast_api_note_1.html)
(更快速、直覺、簡單、自動產生文件)
其實只需要改一點code即可(詳情請參閱github:https://github.com/jkk850429/stock)
start command : uvicorn wsgi:app --reload
起起來之後,這時透過fastapi內建的docs功能便可以打到我們的api


### 接著就要來接上nginx了!!!
(透過brew install nginx指令安裝nginx)
(安裝完的東西會在/opt/homebrew/Cellar底下,若不確定可以下brew list指令)
(改完config記得下 brew services restart <service_name>)
(在Linux世界,systemctl start service_name = brew services start service_name)
nginx.conf內容如下:
將localhost:81轉發到 localhost:8000

在瀏覽器輸入 localhost:81/docs可以轉打到fastAPI的文件頁面了

透過curl成功拿到output股票代碼!!

目前已經準備好我們的 nginx + uvicorn + fastapi,接著準備把它deploy到我們的droplet上了!!預計會走k8s+microservice的方式
## K8S
一般來說我們的service是各自起在各字的server上,彼此再透過內網溝通。但是這個年代沒人在用這套了,太慢太肥大了,後來就演變出Docker

一台server上面可以住很多的Docker Container。但是!如果我們有多台Server,多個Container,該如何管理?這時候就需要**容器編排工具(Container Orchestration) - K8S**了 (p.s.同樣功能還有Docker Swarm/Docker Compose/Redhat OpenShift等等工具)
不囉唆,直接上圖

一個K8S Cluster分為MasterNode(Control Plane) & 一般Worker Node
**Node** : 可以想像為一個實體機器or一台雲端server,一個Node有多個POD
**POD** : K8S世界最基本單位,一個POD有多個container,POD裡面的內網是互通的。ex:我們的POD裡面可以有nginx跟stockprice fastapi兩個container(但為了microservice我會想把他們切開成兩個不同的POD)
p.s. 為何以POD為最小單位?因為如果以container來當最小單位,網路的管理會很難。以 Pod 來區隔,同一個 Pod 裡面的 Container 能夠在本地端互相的連線,只有需要提供給外部呼叫的 API 才需要暴露出來。
(https://hackmd.io/@compalAA/BJgxSoBZs#K8S%E6%95%99%E5%AD%B8%E7%AD%86%E8%A8%98)
假設我有一個多節點的K8S cluster,那所有要加入cluster的node都要安裝:docker/kubelet/kube-proxy/cAdvisor
好,上圖是k8s cluster的架構圖,寫有很多與k8s相關的component。那我們如何從客戶端跟k8s cluster溝通?有一個k8s專用,類似terminal的東西叫做kubectl,我們都是透過kubectl來操作k8s
### Kind (k8s in docker)
一般在學習k8s時會在local端使用minikube,它是單節點k8s cluster(master&nodes在同一台主機上)。為了模擬實際prouction多節點的情況,我們用了另一個叫做"kind"的工具。

透過以下常用指令就可以建立/查看/刪除已建立的k8s cluster / nodes
```
kind create cluster --name clusterName (default叫做"kind")
kind get clusters
kind delete cluster --name clusterName
kind get nodes
```

如果想要透過yaml檔去產生一個cluster,該怎麼做?
1. 準備好yaml檔(~/kind-practice/kind-example-config.yaml)
2. cd至yaml所在目錄
3. 執行 kind create cluster --config kind-example-config.yaml
透過docker ps可以發現kind底層就是透過docker containers來實作出多節點的k8s cluster

------------------------------------------------------
中場休息,安裝一下kubectl唄,安裝完就可以對k8s cluster下指令了
```
brew install kubectl
kubectl run nginx --image=nginx --restart=Never
kubectl get pods
kubectl describe pod my-nginx-pod
kubectl delete pod my-nginx-pod
kubectl rollout restart pod my-nginx-pod
```
kubectl有分為kubectl client跟kubectl server
kubectl client : cli
kubectl server : 就是k8s cluster的kube-apiserver
(建立POD可以透過上面講的kubectl run指令,也可以透過yaml檔的方式。)
------------------------------------------------------
**對k8s有基本的了解後,接下來重頭戲來了:要來把我們的東西deploy到k8s上了!!**
現在,我們有了一個透過kind產生出來的k8s cluster,我要透過kubectl去產生一個POD(base on 我們打包好上傳至dockerhub的fastapi image)
**step0 : 將我們的選股fastapi打包成image上傳至Dockerhub**
準備Dockerfile -> docker login -> docker build -> docker push




[筆記:第一行from指的是base image。client在用這個image建立POD時,Docker Daemon(Docker 引擎)會根據指定的基礎鏡像,從 Docker Hub 或其他指定的鏡像存儲庫中拉取相應的鏡像。如果找到基礎鏡像,它將被下載到本地的 Docker 主機上,然後在其上進行進一步的構建]
**step1 : 先確認kubectl連接到kind的cluster**
```
kubectl config get-contexts 列出所有cluster
kubectl config current-context 目前kubectl指到的cluster
```

代表我們透過kubectl下指令式操作到kind-kind這個cluster沒錯
**step2 : 建立POD配置檔 (yaml檔)**

這邊的containerPort:8000代表什麼?
具體來說,當您將這個 Pod 創建在 Kubernetes 中時,Kubernetes 會在 Pod 中創建一個名為 my-stock-fastapi 的容器,並將容器內部的埠號 8000 映射到 Pod 的 IP 地址上,這樣其他容器或服務就可以通過 Pod 的 IP 地址和埠號 8000 訪問 my-nginx-container 容器中運行的服務或應用程序。
請注意,containerPort 只是在 Pod 內部定義容器對外暴露的埠號,它並不決定您的 Pod 是否能夠從集群外訪問。若要從集群外訪問 Pod 中的服務,您還需要在 Kubernetes 中使用 Service 或 Ingress 等資源將流量路由到相應的 Pod 中。
**step3 : 建立POD**
```
kubectl apply -f mystock-fastapi.yaml
```
過一下子就會看到POD狀態變為running了

------------------------------------------------
[插播]如何與POD互動?
1. kubectl exec -it my-nginx-pod -- bash --> 直接連進POD的bash
2. kubectl port-forward my-stock-fastapi 1235:8000 & --> 本機的port去mapping到pod上的port,藉以從外部去呼叫pod內的應用程式,這方法只適合本機開發時使用(要先透過kubectl apply把POD長出來再port-foward)
[docker版 : docker run -d -p 1234:8000 jkk850429/mystock-fastapi]


**3. 用service (這邊很重要)**
當我們的workload(即AP)上到K8S會發現一個問題:POD的生命週期不是永久的,當我POD重啟後IP就會變,因此如果是傳統直接連某個IP的話那重啟就會連不到了。因此,透過"Service"來解決這個問題
### Service
簡單來說,就是在POD前面再串一個抽象層
Client只需要告訴 Service 我的請求而不需要知道誰會幫我處理也不需要知道處理的過程和細節,Service 收到請求後就會依照定義的規則將請求送到對應的 Pod (POD & POD之間也會透過Service來存取)

技術上如何達到?我們會透過yaml檔產生一個Service的object,yaml檔裡面透過selector來決定要把流量送到哪個label的POD的哪個port
3.1 先把我們的my-stock-fastapi這個POD加一個label
[方法一]直接新增:
```
kubectl label pod my-stock-fastapi app=getStockList-API
kubectl describe pod my-stock-fastapi
```

[方法二]修改POD的yaml

3.2 建立Service物件,透過"**selector**"來link到POD
```
cat my-service.yaml
```

```
kubectl appy -f my-service.yaml
kebectl get services
kubectl describe service my-service
```

POD跟Service都起好了(可以透過kubectl get pods/services確認)
```
kubcectl get endpoints my-service
```

打開瀏覽器,輸入localhost:1266

登愣!爆開了,為什麼連不到!!!??
因為my-service的type不是NodePort or LoadBalancer(default type是ClusterIP,這個只在K8S可見,外部client連不到)
透過port-forward servcie的方式去mapping到service內部
```
kubectl port-forward service/my-service 1266:1266
```
透過這指令就會把我電腦本機端的1266映射到service的clusterIP的1266,接著service會把它轉發到它對應的endpoint
(clusterIP就是該service在k8s cluster的位址,一個service只會有一個clusterIP,但是它可以有多個port,分別link到不同的endpoint,如下圖)

可以了

小結 : browser --(port-forward)--> service的clusterIP的某個port -> Endpoint -> AP
**However,這個做法只適用於local端做測試,實際應用上不會使用port-forward**
注意,一般來說如果Service type是NodePort的話可以透過Nodeip:nodeport來將request轉發給service。但是因為我們用的是kind,Nodeip會是某個docker image的ip,因此在本機端透過這個ip+nodeport是連不到的,所以我們在本機練習時應該要透過port-forward去將各個worker node的port映射在本機
"NodeIP一般會是某台實體機的IP,大家都access的到。但因為我們是用kind模擬出來的,所以NodeIP會是該container的IP(只有在k8s內部的其他node才access的到)"
該如何應證這點?
首先我們在本機透過docker ps / docker inspect #containerhashcode可以得到內部ip
kind-control-plane IP:172.18.0.4
kind-worker IP:172.18.0.5
kind-worker2 IP:172.18.0.3
kind-worker3 IP:172.18.0.2


想到一招了,我再創一個docker container,測試都從它去發curl,應該就打得到了?
[更:可以在kind的config中增加 extraPortMappings的參數就可以了]
(參考網址1 : https://godleon.github.io/blog/Kubernetes/k8s-Service-Overview/)
(參考網址2 : https://hackmd.io/@tienyulin/kubernetes-service-deployment-ingress)
(參考網址3(ClusterIP,裡面有其他連結至其他type) : https://www.hwchiu.com/kubernetes-service-ii.html)
--------------------------------------------------------------------------------------
### Deployment
好了,我們現在已經大致上了解怎麼把我們的AP部署到k8s上面,複習一下
```
將AP打包image -> 透過kubectl apply產生POD
```
接下來要講一個k8s世界中的另一個資源類型 : **Deployment**。通常我們的AP不會直接透過kubectl apply -f my-stock-fastapi.yaml來產生出來,而是會用透過Deployment來管理POD的部署&更新,以下是我問chatgpt關於Deployment的介紹
**通常一個Deployment就是對應一個AP。如果你的k8s cluster內有多個AP,那應該要用多個Deployment來分開管理**


--------
也稍微講一下**replica set** : 它也是k8s的一種資源類型,可以確保指定數量的 Pod 副本在cluster運行,它通常由 Deployment 來創建。
--------
簡單來說,Deployment有三個好處:
1. 自動部署AP : Deployment會透過rolling update來不斷的更新POD & 部署AP
2. zero downtime : Deployment在更新POD的時候會產生一個新的replica set,並在它上面長出新的POD,然後再把舊得replica set的POD刪除,重複以上動作直到所需要的POD更新完成
3. 可以rollback回原版 : 每次進行滾動更新時,會把 Replica Set 紀錄起來,類似於 git 的紀錄。當更新後系統發生問題,就可以回到之前穩定的版本
好接下來就要來實作了,先把前面做的pod刪掉。

先準備好Deployment的yaml

一樣透過
```
kubectl apply -f my-deployment.yaml --record (--record 保留後續的 revision history)
kubectl get deployment
```

```kubectl get pods ```
會發現有三個POD(注意POD的naming會根據deployment的name)

再describe一下會發現這三個POD其實存在不同的node內!

接著練習看看edit deployment
```
kubectl edit deployment my-deployment
```
我把POD的container name改一下,然後save,會發現pod在重長(前面講的rolling update)

**如何查看deployment更新的狀況?**
```
kubectl rollout status deploy my-deployment 查看更新狀況
kubectl rollout history deploy my-deployment 查看歷史紀錄
kubectl rollout undo deploy my-deployment 退回前一版 (--to-revision=<revision number>)
```

**P.S.結合之前講的Service的selector,每個replicaSet長出來的POD會有一樣的label,這樣一來就可以達到load balance的目的**
(參考網址 : https://hackmd.io/@tienyulin/kubernetes-service-deployment-ingress)
### Ingress
前面講到說Service是負責POD前面的一個抽象層,負責來將request導到對應的POD,Service有幾個type : LoadBalancer / NodePort / ClusterIP等等。
問題來了,我們的k8s cluster可能有很多個不同的API/功能,每個可能使用不同的Service。因此如果要讓外部的request打到各自的Service的話,在管理上會很困難(要暴露各個Service的NodePort/LoadBalancer...等等),因此這時候就可以在各個Services前面串一個ingress,當成一個**集中、靈活的方法來管理集群外部流量,並實現多個應用程式的路由和訪問控制**。使得外部訪問變得更加方便、安全和可管理。下面是一些它的好處:

[實作]
假設user在browser網址列輸入
www.waservice/stockprice/ 要能打到我本機端k8s裡面某個pod起的fastapi webservice,該怎麼做?(需透過service)
**step1 : 準備deployment**
```
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
labels:
app: my-stock-fastapi-deployment
spec:
replicas: 3
selector:
# replicaset 的效果套用在帶有 app=getStockList-API 的 pod 上
matchLabels:
app: getStockList-API
# spec.template就是POD的定義
template:
metadata:
name: my-stock-fastapi-pod
labels:
app: getStockList-API
tier: backend
spec:
containers:
- name: my-stock-fastapi
image: jkk850429/mystock-fastapi:latest
ports:
- containerPort: 8000
```
**step2 : 準備service**
```
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type:
NodePort
selector:
app: getStockList-API
tier: backend
ports:
- name: fastapi-port
protocol: TCP
port: 1266
targetPort: 8000
- name: other-port
protocol: TCP
port: 1277
targetPort: 8001
```
**step3 : 準備ingress**
```
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- host: www.waservice.com.tw
http:
paths:
- path: /stockprice/
pathType: Prefix
backend:
service:
name: my-service
port:
number: 1266
```
到這邊有點卡住了,ingress連不到,可能是因為我用KIND的緣故。
因此我們決定來實作真的K8S
**server 1 : ssh administrator@139.59.252.66**
**server 2 : ssh administrator@128.199.175.243**
-----------------
小結 :
Service --> 用於cluster內部流量轉發 & load balance,提供cluster內部 "服務發現"
Ingress --> 用於處理集群外部的網絡流量,將外部的請求路由到集群內部的不同服務

-----------------
## 我的實體K8S
139.59.252.66 | master & worker1
128.199.175.243 | worker2
**整理一些linux常用的指令(centos)**
```
查看service狀態 : systemctl status serviceName
切換為root模式 : sudo su
快速修改文件 : sed -i 's/要替換的字串/新的字串/g' 文件名
查看運行中的service : systemctl list-units --type=service --all | grep kubelet.service
修改檔案的owner : sudo chown
```
請按照以下step來initialize你的k8s cluster
(參考:https://github.com/anishrana2001/Kubernetes/tree/main/Kubernetes%20init)
```
1.查看Mem : cat /proc/meminfo | grep MemTotal | awk '{print ($2/1024)/1024, "GiB"}'
2.查看CPU數量 : cat /proc/cpuinfo | grep processor
3.查看掛載點 : cat /etc/fstab
4.Disable Swap Permanently : sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
5.Disable Swap for this session : swapoff -a
6.Disable the Selinux (SELinux 是一個在 Linux 系統中提供額外安全層的安全機制。) : SELINUX=disabled
7.設定SELINUX模式為Permissive : setenforce 0
8.編輯hostfile : cat /etc/hosts
```

```
9.關閉防火牆 : systemctl stop firewalld.service
10.關閉開機執行防火牆 : systemctl disable firewalld
11.載入br_netfilter模組(for bridge網絡和 Netfilter 相關的功能) : modprobe br_netfilter
12.檢查模組是否有加載到(下圖代表有載入成功) : lsmod | grep br_netfilter
```

```
13.允許在橋接設備上應用 iptables 規則,從而對封包進行過濾和轉發(臨時性的,應該要加到開機執行,需用root執行) : echo '1' > /proc/sys/net/bridge/bridge-nf-call-iptables
14.檢查設定是否成功 : sysctl -a | grep net.bridge.bridge-nf-call-iptables
15.安裝yum-utils : yum install -y yum-utils
16.添加 Docker 的官方 YUM 軟體庫(Repository)到你的系統中(沒加的話沒辦法yum install docker-cli) : yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
17.安裝docker : yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
18.將 containerd 的默認配置輸出並寫入到 /etc/containerd/config.toml 文件中,並將SystemdCgroup=false改為true :
containerd config default | sudo tee /etc/containerd/config.toml | grep SystemdCgroup
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
cat /etc/containerd/config.toml | grep SystemdCgroup
```

```
19.start & enable docker : systemctl enable docker / systemctl start docker
20.試看看docker是否正常 : docker run hello-world
21.將 K8S 的官方軟體庫添加到系統的 YUM 配置中,以便我們可以使用 YUM 套件管理器來方便地安裝、更新和卸載 K8S 相關的套件 :
vim /etc/yum.repos.d/kubernetes.repo
(內容如下)
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.28/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.28/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
22.安裝kubectl kubeadm kubelet : yum install -y kubectl kubeadm kubelet --disableexcludes=kubernetes
```
終於把前置作業做完了,接下來開始來建立我的K8S cluster,因為我的硬體不太夠,要使用一下禁忌的忍術
(--pod-network-cidr=10.244.0.0/16代表使用Flannel CNI作為容器網路介面)
(--pod-network-cidr=192.168.0.0/16代表使用Calico CNI作為容器網路介面)
**kubectl指令都在Master Node執行**
```
23.Init kubeadm : sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=139.59.252.66 --ignore-preflight-errors=NumCPU --ignore-preflight-errors=Mem
```
執行kubeadm init後,系統會做以下的事情 :
- 創建一個 Kubernetes 控制平面,包括 API Server、Controller Manager、Scheduler 等。
- 創建一個加入代碼(Join Token),以便其他節點可以加入到這個 Kubernetes 集群。
- 創建一個 Kubernetes 配置文件,以便您在叢集上執行 kubectl 操作。
- 生成一些密鑰,用於叢集通信和安全。(後續要重新產生可以透過kubeadm token create --print-join-command)

**kubeadm join 139.59.252.66:6443 --token 99yxwk.64tz02iulyveft9f \
--discovery-token-ca-cert-hash sha256:697001e6ea83d3a5f512d0d1859841306833814b2bb52ac4c2952ffa09549719**
(這串要記起來,之後workerNode要加入k8s cluster要用到)
接著我們需要做一些設定讓我們kubectl可以指到這個k8s cluster
(optional) rm -rf $HOME/.kube
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) config
(上述動作要做完後,後續kubelet才起的起來,kubectl指令也才可以執行)
```
24.enable / start kubelet : systemctl start kubelet / systemctl enable kubelet
25.查看kubelet.service是否有起起來 : systemctl list-units --type=service --all | grep kubelet
```

接著就可以來try一下kubectl指令
```
26.查看目前kubectl連到哪個cluster : kubectl config current-context
27.查看k8s cluster的node : kubectl get nodes
```

此時會發現我們master node狀態為NotReady。主要原因是還需為我們的k8s cluster新增網路Addon,node間才可以進行通信。
不擔心,我們先來處理workerNode
### [Task] 將我們的第二台server加入到這個k8s cluster中
需求 : 請參考我上面寫的以及https://github.com/anishrana2001/Kubernetes/blob/main/Kubernetes%20init/script%20for%20workernodes
把第二台server加入到我們的k8s cluster中!!!!
hint : 產生一個.sh檔,內容參照上面網址,並修改hostfile內容以及kubeadm join用的token
我們在執行kubeadm join時遇到了問題(Found multiple CRI(container專用的)),嘗試加上
--cri-socket=unix:///var/run/containerd/containerd.sock (這邊我們選擇containerd)

成功了!! 再到masterNode去kubectl get nodes看看

把Node:centos-my-droplet-2的Label改為 WorkerNode:
```
kubectl label node centos-my-droplet2 node-role.kubernetes.io/worker=worker
```
有了,但是狀態為NotReady,可以透過kubectl describe node nodeName來看看為什麼(要在masterNode下)


發現它說我們CNI還沒initialize
回到MasterNode
來安裝一下網路插件,這邊我們選擇flannel
(這邊做完可以重開機一下)
```
28.安裝網路Addon(flannel) : kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
(有興趣可以curl一下看看這個yaml長怎樣)
29.查看所有namespace的pods : kubectl get pods -A
```


可以發現多了kube-flannel這個namespace,底下有2個flannel的pod,而且還有一些masterNode的component(綠框處),原來他們也都是一個一個的POD!!!
這時候我們create一個POD會發現它還沒辦法被指派到Node上(Status會是Pending),主要是因為我們目前只有masterNode,而且他有設定taint為NoSchedule(表示不允许非容忍的Pod被調度到這個帶有Taint的Node)

### 如何切換context(kubeconfig)
從我的Macbook透過kubectl config current-context會得到kind-kind,代表我在Macbook輸入kubectl指令都是對local端的kind-kind cluster做操作,那該如何切換至我的雲端k8s cluster呢?
先了解一下context是什麼組成的

路徑 : ~/.kube/
我的電腦原本有一個config是連到local的kind-kind,我去server1把它上面的config拉到我的本機,所以我現在有兩個config

透過以下指令將config檔merge
```
KUBECONFIG=~/.kube/config:~/.kube/my-cloud-k8s-config kubectl config view --merge --flatten >> ~/.kube/config
```
再透過
```
kubectl config get-contexts
kubectl config use-context kubernetes-admin@kubernetes
```
成功!!!

因為我有準備Type為NodePort的service,所以這時候透過NodeIP + NodePort就可以轉發到service的clusterIP+Port再轉發到POD的targetPort

## Telegram Bot
我們的目標是做一個機器人聊天室,裡面有幾個功能可以選
1. 產生候選股票代碼
2. 維護參數資料
點擊功能(1)可以打到我們寫好的fastapi去拿到資料,並顯示在聊天室當中
點擊功能(2)可以跑出一個視窗讓user填入資料,並且更新Database中的資料
在這邊我們選擇使用Telegram Bot來實作!
**1. 先來跟BotFather聊個天,即可建立我的bot**
```
/start
```

把WawaStock_bot的Token記錄下來
6585401439:AAFDdGcy-bDH-LilYOmuvxE69dB0qHuW6Yw

**2. 設定一下機器人的介紹**
```
/setdescription
/setabouttext
```


**3. 設定照片**
```
/setuserpic
```

可以搜尋一下WawaStock_bot

**4. 寫code**
```
pip install python-telegram-bot
```
telegram_bot常用的一些函數:
"update物件" : 包含了接收到的用戶消息和其他相關的信息的一個object
update.message.reply_text :
sample code 如下
```
from typing import Final
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
TOKEN: Final = '6585401439:AAFDdGcy-bDH-LilYOmuvxE69dB0qHuW6Yw'
USERNAME: Final = 'WawaStock_bot'
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text('Hello, thanks for chatting with me! I am Wawa!')
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text('I am Wawa, please type something so I can respond.')
async def custom_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text('This is a custom command!')
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
message_type: str = update.message.chat.type
text: str = update.message.text
print(f'User ({update.message.chat.id}) in {message_type} : "{text}"')
if message_type == "group":
if USERNAME in text:
new_text: str = text.replace(USERNAME, '').strip()
response: str = handle_response(new_text)
else:
return
else:
response: str = handle_response(text)
print("Bot:", response)
await update.message.reply_text(response)
async def error(update: Update, context: ContextTypes.DEFAULT_TYPE):
print(f'Update {update} causes error {context.error}')
def handle_response(text: str) -> str:
processed: str = text.lower()
if "hello" in processed or "你好" in processed :
return "Hey Hi"
if "how old are you" in processed or "你幾歲" in processed:
return "我5歲"
if "比較大" in processed:
return "大娃娃,因為他5歲"
if "比較小" in processed:
return "小娃娃,因為他才3歲"
if "小娃娃" in processed and "歲" in processed:
return "3歲"
return "I can't understand what are you talking about...."
if __name__ == '__main__':
print('starting BOT')
app = Application.builder().token(TOKEN).build()
# Commands
app.add_handler(CommandHandler('start', start_command))
app.add_handler(CommandHandler('help', help_command))
app.add_handler(CommandHandler('custom', custom_command))
# Messages
app.add_handler(MessageHandler(filters.TEXT, handle_message))
# Errors
app.add_error_handler(error)
# Polls the BOT
print("Polling...")
app.run_polling(poll_interval=3)
```
5. 打包丟到k8s
**用我們之前所學的方法:透過Dockerfile打包成image -> 寫pod yaml並透過kubectl apply指令起起來**
docker build -t jkk850429/bugobird_bot:latest .

發現有問題,POD一直起不起來,透過kubectl logs PODName可以看log
發現python3 run不起來,在build的時候加一個東西
**猜測可能是mac m1晶片build出來的image無法在linux環境執行,只後後續的練習都加上 --platform=linux/amd64了**


**[2024.1.14新增 getStocklist指令,後續可以透過/getStockList來取得股票代碼]**
在布股鳥 app中新增以下段落 -> 透過Docker build產生新的image


接著要更新我們k8s上面的布股鳥POD,試著透過之前講的deployment rolling update的特性
(因為沒有更改到deployment.yaml,所以kubectl apply -f my-bot-deployment.yaml他會說unchanged。因此敲kubectl rollout restart deployment my-bot-deployment)
會看到新舊同時並行

過一陣子只剩新起的deployment

## NodePort service 到 Ingress Gateway
複習一下:前面有提到說我們目前是透過NodePort的方式從外部access到POD,也就是
```
client -> NodeIP+NodePort(30588) -> service clusterPort(1266) -> POD的某個Port(8000)
```


這有一個問題:NodePort用在測試時很方便,但是正式環境我們不會這麼做。正確做法應該是要在網址列輸入"https://bmb-service.com/getStockList/stockprice"才對,而不是"http://139.59.252.66:30588/stockprice"。除此之外,一但我的service重啟,NodePort就跑掉了,不就access不到了?因此我們得想個辦法,不該使用NodePort的方式!!因此,我們就必須要adopt "Ingress" 這個東西,他的概念如下

以下我們開始來嘗試加上ingress吧!
1. 首先要使用Ingress資源的話,需要先安裝Ingress Controller,只單獨創建Ingress是沒有用的!我們這邊選擇使用"Nginx Ingress Controller" (官方:https://kubernetes.github.io/ingress-nginx/deploy/)

他有幾種安裝方式

因為我們還不會helm chart,所以使用第二個方式kubectl apply
```
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml
```
安裝完ingress contrller後透過以下指令確認有安裝成功(它也是POD~)
```
kubectl get pods -n ingress-nginx
```

這兩個 Pod ingress-nginx-admission-create-w5g7t 和 ingress-nginx-admission-patch-cszcm 是用於處理驗證和自動創建 Ingress 資源的工作負載。一旦它們完成了它們的任務,它們就會成功地退出並且狀態變為 Completed。這是正常現象,不必擔心。
2. 創建一個新的service
因為我們之前用的是NodePort,避免搞混我再create一個新的service
(my-service-byclusterip)


3. 創建Ingress (透過以下yaml)
```
kubectl apply -f my-ingress-nohostname.yaml
```

4. 從瀏覽器網址列打看看 - 失敗QQ
http://139.59.252.66/getStockList

## MetalLB + Nginx Ingress
bare metal裸機K8S cluster沒有cloud供應商提供的load balancer,可以用MetalLB代替,步驟如下:
**[前置作業]**
1.
```
kubectl get configmap -n kube-system
```

2.
```
kubectl edit configmap kube-proxy -n kube-system
```

修改完後:wq存檔退出
3.
```
kubectl get pods -n kube-ststem
```
查看發現未生效,透過kubectl rollout指令即可

**[安裝metallb]**
1.
```
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.9/config/manifests/metallb-native.yaml
```
安裝並生效metallb後,可以發現多了一個namespace metallb-system

透過kubectl get pods -n metallb-system可以看到他幫我們創建了幾個pod (controller & speaker)

查看相關resource

2. 定義IPAddressPool
~/Application/Git_Repo/metallb底下
- 2.1 IPAddressPool

```
kubectl apply -f my-metallb-ipaddresspool.yaml -n metallb-system
```
```
kubectl get IPAddressPool -n metallb-system
```

- 2.2 L2Advertisement


**3. 此時可以看到我們新建的loadbalancer service拿到ip了!**

(LoadBalancer Service yaml如下圖)

(透過URL輸入:www.bmb-service.com:9999/docs)

目前可以透過ip + loadbalancer port打到POD了,但是當pod一多,這樣都透過port也不是辦法。我們的終極目標是要在外網browser輸入"www.bmb-service.com/getStockList"。因此要再來加上**Ingress**
**[加入nginx controller]**
在這之前,先把原本loadbalancer service砍掉
1. 做法同之前所做的先安裝nginx ingress controller
```
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml
kubectl get services -n ingress-nginx
```

可以發現有external ip了!
2. 創建一個type為"clusterIP"的service


3. 創建Ingress,內容如下

```
kubectl apply -f my-ingress.yaml
```
4. 測試
在URL輸入: www.bmb-service.com/getStockList/
好像快成功了(已經有打到POD)

但是輸入 www.bmb-service.com/getStockList/stockprice 沒有打到fastapi(一樣是Not found畫面)
www.bmb-service.com/getStockList/docs也沒東西

可以知道www.bmb-service.com:30588/ = www.bmb-service.com/getStockList/
**``後來發現是re-write annotation的問題(詳情見小筆記區),搞懂re-write function後就可以了``**
5. OK最後的步驟就是把ingress變成regex的匹配方式

如此一來在URL輸入 bmb-service.com/getStockList/stockprice/ 就會轉到/stockprice/的api

輸入bmb-service.com/getStockList/random_number/ 就會轉到random_number的api

(注意:最後面的 / 不能少)
為了避免混淆,把/getStockList改成/bcfast代表我的整支fastapi

6. 最後,把布股鳥打的api url改成新的ingress版本的!重restart deployment

``
kubectl delete deployment kubectl delete deployment my-bot-deployment
kubectl apply -f my-bot-deployment.yaml
``
成功!!!!

## 加入Istio進行流量管理
### Service Mesh (參考https://ithelp.ithome.com.tw/articles/10289605)
**可以把 Service Mesh 理解為 Kubernetes 的擴充套件,安裝 Service Mesh 方案到 Kubernetes 後,就能強化叢集的流量管理能力、可觀測性以及安全性。**
K8S不會追蹤Pod間的溝通,因此如果有一元件有問題,難以trouble shooting

安裝Service Mesh後,Pod間的溝通皆須透過此層集中化管理,蒐集Pod 的連結情形,或是設置 Pod 之間的路由規則,使其在不修改服務的情況也能達到流量管理、可觀測性等功能。
``
Service Mesh 抽象出一個層級,不僅能紀錄 Pod 之間溝通情況,也能管理 Pod 的溝通行為
``
常見實作Service Mesh方式為Sidecar模式,每個Pod除了AP的container外,還會附加一個proxy container,對外的需求皆交由proxy代理。
實作上需要建立兩個抽象東西:
1. Control Plane : 負責規劃、分配
2. Data Plane : 實際執行任務 (即上面提到的Sidecar)
對Control Plane下指令 --> Control Plane將規則下給Data Plane,由proxy實際執行
(Sidecar Proxy是一種架構、模式,Envoy proxy是實際上最常用的proxy軟體)

### Istio介紹
Istio核心三功能:
1. 流量管理 : 配置規則來控制服務間的流量,並能執行Canary Deployment. A/B testing等任務
2. 可觀測性 : 透過Log aggregation. Metric. Tracing功能可結合視覺化軟體,增加trouble shooting能力
3. 安全性 : 提供底層的安全通信管道,並管理認證、授權、加密,讓開發者只需專注於應用程式的安全。

**kube-proxy v.s. Service Mesh**

(https://ithelp.ithome.com.tw/articles/10293126)
kube-proxy的缺點 :
1. 攔截的是進出節點的流量,而 Sidecar 攔截的是進出 pod 的流量,Kube-proxy 沒辦法對服務更精細的控制。
2. 若 Pod 不能正常服務(Ex: HTTP 500 Error),Kube-proxy 無法偵測到,也就無法嘗試重啟 Pod。
3. 只能對 Pod 作 rr、隨機分發等負載平衡機制,沒辦法對流量進行細緻的控制(Ex: 99%流量給A,1%流量給 B)
### 安裝Istio & 設定至K8S cluster
0. 下載並安裝istio (參考https://ithelp.ithome.com.tw/articles/10290660)
1. 下istioctl version確認是否安裝成功
2. istioctl install,即可安裝至kubectl指向的K8S cluster (context)

(這些就是control plane的元件)
3. 在 bugobird namespace 設定 istio-injection=enabled Label
```
kubectl label namespace default istio-injection=enabled --overwrite
kubectl get namespace -L istio-injection
```

這個動作代表告訴 Istio:
👉 在這個 Namespace 裡新建立的 Pod,都要自動注入 Sidecar Proxy(通常是 Envoy)
(但是已經起起來的POD不適用,因此須砍掉重起)
4. 刪掉POD讓它重長,發現變成2個container了 (但是有問題要解決一下)



參考以下文章 https://www.cnblogs.com/fanqisoft/p/17173326.html
再刪除pod後重長後修好了

kubectl describe一下,發現裡面有兩個container了!! (一個AP的,一個istio-proxy)

### Istio實作
[練習] Bookinfo專案 : 參考(https://ithelp.ithome.com.tw/articles/10293752)
(https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/platform/kube/bookinfo.yaml)


1. create namespace : kubectl create ns bookinfo
2. 設定istio-injection label來允許sidecar注入
```
kubectl label namespace bookinfo istio-injection=enabled --overwrite
kubectl get namespace -L istio-injection
```
3. 部屬bookinfo APP
```
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.15/samples/bookinfo/platform/kube/bookinfo.yaml -n bookinfo
```

4. 架構圖

5. 透過port-forward
```
kubectl port-forward svc/productpage 8080:9080 -n bookinfo
http://localhost:8080/productpage
```

每次刷新這邊都會變,因為service label-selector的關係
**後續會透過Istio的兩個元件 : DestinationRule 及 VirtualService 來達到流量的切換**
6. 建立destination_rule.yaml & virtual_service.yaml

spec.host 定義要對 Reviews Service 流量設置規則

spec.host 定義要將 Reviews Service 的流量做分群
apply一下,再get就會發現這兩個元件跑起來了

因為virtualservice的subset指到v2,因此會發現review會一直打到v2


把subset改成v3再重新apply後會變成打到v3

### VirtualService & DestinationRule介紹
Istio如何管理流量?就是透過這兩個元件
**VirtualService -> 定義流量怎麼走**
**DestinationRule -> 定義目的地為何**



### Istio 應用至選股API
**目標 : 讓外部透過browser輸入URL:www.bmb-service.com/getStockList 打到我的fastapi pod**
首先讓istio能注入stockpickup namespace,並砍掉deployment重新apply一次
```
kubectl label namespace bookinfo istio-injection=enabled --overwrite
kubectl get namespace -L istio-injection
```

接著準備gateway以及virtaulservice yaml檔


```
kubectl apply -f stockpickup-gateway.yaml -n stockpickup
kubectl apply -f stockpickup-vs.yaml -n stockpickup
```

這時候在URL輸入: www.bmb-service.com/getStockList 發現失敗了!!! (出現nginx error畫面)
kubectl get svc -A會發現ingressgateway 沒有拿到externalIP

按照前面方式apply metallb後,拿到externalIP了!!!

但還是失敗!!

做一些調整
vs yaml

可以了!!!感動

順便把bugobird改一下

## K8S dashboad
每次都要透過terminal下kubectl xxxx 來操作k8s cluster,超麻煩。來建立個dashboard吧
1. 安裝k8s dashboard
```
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
```
會安裝在kubernetes-dashboard namespace 底下
2. 建立admin service account & clusterRoleBinding (準備以下的yaml)

3.取得token
```
kubectl -n kubernetes-dashboard create token admin
```

### 方法一:透過kubectl proxy來連dashboard
```
kubectl proxy
http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/
```

輸入token

可以了!!!
### 方法二:
## 小筆記區
### digital ocean server 綁定 DNS
1. 先至GoDaddy購買一個域名 (www.bmb-service.com)
2. 擁有DNS後,設定方法如下 (https://www.morrisctech.com/content/2018/08/21/bind_domain/)
### 刪除metallb
有點複雜
1. 刪掉namespace (可能會卡住,需用force指令)
2. 還會有髒資料,還要刪除kubectl get crds | grep metallb的東西
### init server 工作
https://hackmd.io/@laynotena/S1nXGjI52
### initialize K8s cluster
kubeadm reset 下完要砍掉$HOME/.kube資料夾
### 關於nginx ingress controller的annotation:
nginx.ingress.kubernetes.io/rewrite-target : /
代表的意義是什麼?
它會將url re-write,假設我的ingress yaml檔長這樣

那我在URL輸入www.bmb-service.com/getStockListxxxxxxx(因為pathtype是prefix),就會被
1. 轉打到my-service-byclusterip
2. url re-write成 www.bmb-service.com"/"
所以結果就會變成打到主頁

那假設我把yaml檔改成這樣

那我在URL輸入www.bmb-service.com/getStockListxxxxxxx就會
1. 一樣轉打到該service
2. URL re-write成 www.bmb-service.com/random_number/
因此結果會變成

### 更新區
2024.02.28 server2換ip : 143.198.83.197
2024.03.24 更新StockPrice.py,新增兩個interface "/"跟"random_number"
### 布股鳥指令大全
/getRandomNumber
/getStockList