---
# System prepended metadata

title: 建立K8S到可以部署並提供服務的歷程
tags: [K8S, AKS, Terraform, ArgoCD]

---

這篇文章主要是講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



