這篇文章主要是講YSDT這邊上Azure之後從安裝AKS直到工程師可以部署服務為止的所有動作與考量。等於是 **Infra on AKS**。
>由K8S章節開始就可以應用在所有的K8S, 包括 GKE, AKE, OKE, 或地端的K8S.
## 架構圖 (2024/02)

## 事前
* 一個AKS帳號,權限可以建立大多數的資源
* 對`Terraform/OpenTofu`有概念或用過,另外要有`Terraform cloud`的帳號並加入YSDT群組(optional), 可以用來協作。
* 有`gitlab.com`的帳號,並已加入`YSDT-ai-dev`群組。
## 準備
* 先把 [Azure Infra](https://gitlab.com/ysdt-ai-dev/azure_infra) 用 `git clone` 抓下來備查.
* 從終端機登入 `azure` 與 `terraform cloud`:
* Azure: `az login`
* Terraform Cloud: `terraform login`
> 建議: 如果在azure同時有多帳號切換的話,最好用`export AZURE_CONFIG_DIR` 來切。記得先做這件事再 `az login`, 才會把帳號細節寫入 export 出來的資料夾。
> 建議: 就算是用 `OpenTofu` 也還是要做 `terraform login`。
## 建立 AKS
建議先拿Clone下來的 `azure_Infra/MACC/aks_k8s03` 作參考。
基本上在建立AKS之前需要先有 `VCN` 以及在上面建的 `Subnet`。
Terraform裡的建法可以參照目錄下的`README.md`。比較需要注意的是,需要使用Storage與ACR(Azure Container Registry)的部分要授權,這是AKS的專屬部分。
**AKS/K8S 功能還在進步中,這裡講的比較一般性**
> 如果在建立AKS時,Network Driver是選用 `Azure CNI`: 這表示這座K8S的網段會佔用該座K8S指定的subnet,也就是在這座K8S的Pod都會配發一個這個 subnet裡的IP。因此用這個方式的話,Azure會要求所在的subnet IP要夠多 **(一個K8S應該最多可以扛5000個Pod)**
> 如果Network Driver是選`kubernetes`,Pod 就不會用到 subnet 的 IP(但Service有可能),只是如果pod需要存取AKS之外的資源,比如 `postgreSQL`,直連就有可能連不到(如果該資源設成只限同一VCN的client來連的話)。
### AKS的連線憑證
一般方法有兩種:
1. 由 Azure Portal
* `export KUBECONFIG=<要建的kubeconfig檔>`
* 由 Azure Portal裡K8S資源的 `overview->connect` 就可以看到下載 `cluster credential` 的方式。
* 憑證會寫到 `要建的kubeconfig檔`
2. AKS由 `terraform apply` 建立的
* 到該terraform目錄
* `echo "$(terraform output kube_config)" > <你的KUBECONFIG檔>`
* 進`<你的KUBECONFIG檔>`,把`EOT` 跟 `<<EOT` 兩行刪除
> **因為這個憑證權限非常大,所以最好是看下文的建立帳號方式,用較小權限的憑證來維護或配發**
> **雖然kubeconfig檔可以容納多個cluster與user的憑證,並利用 set-context 指令切換,但為了不小心誤用,建議一個config檔一座K8S一個帳號就好,切換時用 `export KUBECONFIG` 到指定的K8S/User**
> **如果忘了做 `export kubeconfig` 的話,預設會用 `~/.kube/config`!**
### 選用nodepool的VM Size
`Nodepool` 是指一個可以用來自動增減node個數的VM池。所以它會需要設定VM Size(就是VM的等級)。一個AKS可以設定多個nodepool,每個nodepool的VM Size不一定要一樣。VM Size的選用是根據成本考量與pod需要的能力(包括pod設定的resource requirement/limit值)
**但是要注意設定的最小與最大node個數**
因為可以隨時再加nodepool,所以你可能在一開始沒有選最佳的nodepool。但是**我們並不太知道Azure是怎麼分配資源**,所以最好還是挑選夠力的VM Size,或是及時調整nodepool的組態,比如換好一點的。
### System nodepool 與 User nodepool
>依照Azure的定義,System nodepool只能有一個,User nodepool可以沒有或很多個。
跟上面一樣。因為**我們並不太知道Azure在System nodepool與User nodepool之間是怎麼調配的**,頂多是知道System nodepool會固定存在來放系統的pod(但我們也發現系統的pod也會放在User nodepool,而我們部署的程式也會被放在System nodepool裡),為了我們的部署生態穩定,如果同時都有System nodepool或User nodepool的話,最好VM Size設成一樣會好一點。
### 更換或更新nodepool
如果撞到以上講的狀況,或臨時要更換nodepool的VM Size的話,可以線上更換nodepool本身。
>這在一般`K8S`也會碰到,就是node更新或更換。在Azure因為node是nodepool長出來的,程序多一點(在nodepool換一個node等於沒換,因為新node的組態沒變)
**建議用指令跟Azure Portal做這件事,做完再update terraform上的設定**
1. 先建一個新的nodepool,選用新的VM Size,這個就是未來要用的。
2. 用 `kubectl get nodes` 列出所有的node,確定要換的nodepool長出來所有node的名稱
3. 用 `kubectl cordon <nodename>` 停用每一個node。這代表不會有新的pod被配發到這個node,但node上的pod還是繼續在跑。
4. 用 `kubectl drain <nodename> --delete-emptydir-data --ignore-daemonsets`把node上的pod停掉,它們會在其他node再長出來。
**這個指令要多下幾次,或等個一兩分鐘再下一次,確定可以移的都移走了。剩下幾個還在running的pod很正常。**
5. 確定`4`完成後就 `kubectl delete <nodename>`。
6. 等到舊的nodepool所屬的node都刪完之後,就可以去Azure Portal刪那個nodepool。
7. 記得更新你的`terraform.tfvars`, 把nodepool名稱換成新的這個,以免下次作`terraform apply`時不小心刪掉它。
***-- -- -- 分隔線 -- -- -- 接下來可以應用在一般的K8S -- -- --***
## K8S基本常識
如果你剛接觸K8S不久的話,至少要知道:
* K8S上的所有資源都可以用 `yaml` 格式的檔案來描述,並以`kubectl apply` 或 `kubectl apply -f <yaml file>` 配發到K8S上。
* **A K8S is a kind of management of Docker.** 所以K8S的部署概念大多是延續自Docker引擎。
### Deployments, Pods and Services
**Deployment** 是用來描述如何部署一個應用,或是這個應用如何使用資源。比如
```
apiVersion: apps/v1
kind: Deployment
metadata:
name: onedriveshare
namespace: apps
spec:
replicas: 1
selector:
matchLabels:
deployment: onedriveshare
template:
metadata:
labels:
deployment: onedriveshare
spec:
imagePullSecrets:
- name: gitlab-secret-ysdt
containers:
- name: onedriveshare
image: onedriveshare
imagePullPolicy: Always
ports:
- containerPort: 9000
volumeMounts:
- mountPath: /var/www/html
name: nginx-www
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "cp -rp /app/html /var/www; cd /var/www/html; php artisan storage:link"]
preStop:
exec:
command:
- sh
- '-c'
- sleep 5 && kill -SIGQUIT 1
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- mountPath: /var/www/html
name: nginx-www
- mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
name: nginx-config
lifecycle:
preStop:
exec:
command:
- sh
- '-c'
- sleep 5 && /usr/sbin/nginx -s quit
volumes:
- name: nginx-www
emptyDir: {}
- name: nginx-config
configMap:
name: nginx-config
```
**Pod** 是被 **Deployment** 部署的應用的執行單元。K8S會**動態**部署Pod到適當的node。也就是說,Pod有機會被K8S系統隨時**好好的**搬移到其他的node。
> 有些Pod不會被移,比如 `DaemonSet` 的Pod, 這不在本文討論範圍。
> 一個Pod可能包含多個 `container`, 一個`container` 裡面包含一個`docker image`。這些都定義在**Deployment**裡(如上面的範例)。
**Service** 是**Deployment**部署的應用要公開給外界呼叫的端點描述。除此之外它也通常為這個應用內的所有**Pod**作負載平衡。**Service**一般會有三種型態:*ClusterIP*, *NodePort*, *LoadBalancer*。
*ClusterIP* 它就是取一個K8S本身IP池的IP, 當Pod的loadbalancer。所以它一般只能提供服務給在同一座K8S的Pod,除非是用forward的方式把內部IP/PORT對應到本機上的某個PORT。也有另一種*ClusterIP*沒有定義IP, 這種叫*Headless*,只能用名稱來呼叫,通常會用在StatefulSet。如:
```
apiVersion: v1
kind: Service
metadata:
name: mariadb-service
labels:
app: mariadb
spec:
ports:
- port: 3306
name: mariadb-port
clusterIP: None
selector:
app: mariadb
```
*NodePort* 則是會給自己設定一個PORT,外界則透過node的ip加上這個PORT做呼叫。不過它沒有負載平衡的效果,而且**node的ip必須要給外界,製造風險**。
*LoadBalancer* 會有一個外部IP讓外界呼叫。
> 一般在ACK的使用情境會是:
> * 如果是提供內部環境呼叫的API, 會設成 *LoadBalancer* 再加上 `service.beta.kubernetes.io/azure-load-balancer-internal: "true"`,利用網路區隔阻擋外部連線。
> * 如果是提供外部呼叫,會設成 *ClusterIP*,再設定 *Ingress*,由*Ingress*控制流量進入的安全性。
### Configmap, Secrets
這是兩種常用的資料型態,提供給Pod作環境變數存取。因為Pod是由數個`container`裡的`image`啟動的,`image`在應用建立時期就做好了,通常很難在Pod運行之後再改組態。用這兩種資料型態可以保證每個Pod吃的組態都一樣,不用再進每個Pod去改。
> Deployment只能讓Pod去Mount同一個namespace底下的configmap或secret
使用的範例如:
```
...
volumeMounts:
- name: config-logrotate
mountPath: /etc/logrotate
Volumes:
- name: config-logrotate
configMap:
name: nginx-logrotate-config
```
*Configmap* 可以視作每個應用在VM底下執行時,放在 `/etc` 底下的檔案。例如:
```
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-logrotate-config
namespace: ingress-nginx
data:
logrotate.conf: |-
/var/log/nginx/*.log {
daily
rotate 3
compress
delaycompress
notifempty
create 0644 nobody nobody
sharedscripts
postrotate
[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`
endscript
}
```
如果搭配再上一個範例,這個configmap就等於是建一個 `/etc/logrotate/logrotate.conf`, 內容則是這個範例的 `/var/log/nginx/*.log { ... }`
*Secrets*則類似*configmap*, 只是它通常是編碼過的Binary(如金鑰),並用`base64`編碼,**所以不能把它當做是密碼!!!***
### Ingress
### Storage
### Creating roles and users for lower privileges
#### Roles in K8S
#### Creating namespace specific roles and cluster-wide roles
#### Steps for creating a user
## Basic services
### Prometheus
### Nginx Ingress
#### Mod_security
#### Sidecars
##### Logrotate
##### Fluentd
#### ConfigMaps
#### Deploy SSL Keys
##### CertManager and Let's Encrypt
### Keda
### ArgoCD
#### Add an cluster to manage
#### Creating users and roles
#### Concept of managing users' privileges
## Dealing with K8S
### with Prometheus
### with Lens
### kubectl
### helm