這篇文章主要是講YSDT這邊上Azure之後從安裝AKS直到工程師可以部署服務為止的所有動作與考量。等於是 **Infra on AKS**。 >由K8S章節開始就可以應用在所有的K8S, 包括 GKE, AKE, OKE, 或地端的K8S. ## 架構圖 (2024/02) ![ysdt_azure_202401](https://hackmd.io/_uploads/H1I0EQshp.jpg) ## 事前 * 一個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