Google Code Prettify

2020年4月17日 星期五

K8s: Deployment

在「K8s: ReplicaSet」及「K8s: Service」兩篇,已經有說明了,當 docker hub 上有個 image,怎麼將它部署到 Kubernetes 上,並讓外部可以訪問到它。兩個步驟可以合併成一個步驟如下:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: host
spec:
  replicas: 3
  selector:
    matchLabels:
      app: host
  template:
    metadata:
      labels:
        app: host
    spec:
      containers:
      - name: host
        image: twleader/host
---
apiVersion: v1
kind: Service
metadata:
  name: host-svc
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 8080
    targetPort: 9080
  selector:
    app: host
注意看第 18 行,有 --- 將原本兩個 yaml 隔開合併成一個檔,透過這種方式,可以合併多個 yaml 檔,這樣部署可以快一點,但是,對 K8s 來說,這是比較低階的做法,K8s 另外提供了 Deployment 這個更高階部署 rs 的方法,yaml 可以如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: host
spec:
  replicas: 3
  template:
    metadata:
      name: host
      labels:
        app: host
    spec:
      containers:
      - image: twleader/host:latest
        name: host
指令一定要如下,不能忽略後面的 --record !! (原因後面說明)
kubectl create -f xxxx.yaml --record
使用 Deployment 比之前的做法好,這是完全交給 K8s 幫我們決定怎麼部署,除了比較容易外,還可以達到滾動式部署! (Service 還是要另外創建) 假如我們提供的服務不能中斷,換版時希望新版還沒有啟動前,舊版程式要持續提供服務,那麼使用 Deployment,改變第 14 行的版號,讓 K8s 部署新版本,K8s 會先創建新版的 pod 及 ReplicaSet,並等新版的 pod 完全啟動後,再逐步的刪除舊版的 pod。如果要下指令的話,如下:
kubectl set image deployment host host=twleader/host
名稱取的不太好,說明一下,第一個 host 是 deployment 的名稱,接著 host=twleader/host 是指,命名為 host 的 container 要由 twleader/host 這個 image 產生。在滾動更新的過程,如果要知道更新的狀況,可以下這個指令:
kubectl rollout status deployment host
部署好之後,使用 kubectl get po 可以看到舊的 pod 一個一個慢慢消失,使用 kubectl get rs 觀察,則會發現如下,
舊的 ReplicaSet 並沒有被刪除! 這方便當發現新版程式有錯,想要回到舊版時,可以更快速,要怎麼回到舊版呢? 可以使用 undo 如下:
kubectl rollout undo deployment host
事實上,K8s 每次部署都會記錄下每個版本,我們可以回滾到任何一個歷史版本,先看一下 history 指令:
kubectl rollout history deployment host
顯示如下:
之前 create deployment 時有加 --record 的話,這裡的 CHANGE-CAUSE 欄位才會有值,方便我們辨識該版本,我這邊因為都用 create 的方式,如果檔名取不同名稱可以看的出兩個版本的不同,如果是用下指令的,在 image 後面的版號就可以看出版本間的差異,要回到任何一個版本,可以下 undo 並加上 --to-revision 參數:
kubectl rollout undo deployment host --to-revision=2
要特別注意的是,K8s 的版本紀錄是保存在 ReplicaSet 裡,所以舊的 ReplicaSet 不要手動刪除,否則就無法回滾了!

2020年4月16日 星期四

K8s: ConfigMap

前一篇「K8s: 設定環境變數」是利用 create ReplicaSet 時,由 yaml 檔傳入環境變數,萬一我們的系統有許多個 ReplicaSet 要創建,每個 yaml 都要寫相同的設定,這樣就等於是 hard code 在 yaml 裡,維護相當不容易。

Kubernetes 提供了 ConfigMap 的功能,讓環境變數引用 ConfigMap 裡的值,達到集中管理的目的。怎麼建立 ConfigMap 有許多種方法,這裡舉兩個如下:
kubectl create configmap practicecnfmap --from-literal=jasypt_encryptor_password=secondKey --from-literal=spring_profiles_active=uat
這是最直覺的方式,透過命令建立一個名為 practicecnfmap 的 ConfigMap,裡面有兩個設定 jasypt_encryptor_password 及 spring_profiles_active,它們的值在這裡故意和前一篇不同,這樣才能觀察是否 ConfigMap 有產生作用。萬一 ConfigMap 裡要設定很多參數怎麼辦? 不可能在命令列這樣的輸入,可以選擇先把這些參數以 key=value 的方式存在一個目錄,檔名是 key, 內容是 value,我建立了一個 conf 的目錄,裡面有兩個檔案如下:
檔案裡是值,接著輸入如下指令, 這樣得到的結果和上面的指令是一樣的。
kubectl create configmap practicecnfmap --from-file=./conf
接著要修改 ReplicaSet 的 yaml 檔,如下:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: practice-rs
spec:
  replicas: 2
  selector:
    matchLabels:
      app: practice-rs
  template:
    metadata:
      labels:
        app: practice-rs
    spec:
      containers:
      - name: practice-pod
        image: harbor.steven.idv.tw/steven/practice-pod
        envFrom:
        - configMapRef:
            name: practicecnfmap
      imagePullSecrets:
      - name: steven-harbor
第 18~20 行指出,環境變數由 practicecnfmap 這個 ConfigMap 取得。建立 ConfigMap 後,如果想要修改內容,不需刪除後重新建立,可以用以下指令編輯。
kubectl edit configmap practicecnfmap

2020年4月15日 星期三

K8s: 設定環境變數

spring.profiles.active=${spring_profiles_active}
server.servlet.context-path=/
server.port=8080

# encrypt
jasypt.encryptor.bean=encryptorBean
jasypt.encryptor.password=${jasypt_encryptor_password}
jasypt.encryptor.algorithm=PBEWithMD5AndDES
spring boot 的程式,設定多半寫在 application.properties,裡面的值基於資安,或是其它各種理由,可能不會直接寫在檔案裡,而會取自環境變數,如上的 application.properties 有兩個參數來自環境變數,在 K8s 的環境要怎麼傳入環境變數呢?

這裡簡單的寫一支 REST API,會傳回 application.properties 裡的值,看看那兩個取自環境變數的值是否正確,程式如下:
@RestController
@RequestMapping("/info")
@Slf4j
public class InfoController {
    @Autowired
    private Environment env;
    
    @Autowired
    ResourceLoader resourceLoader;
    
    @GetMapping("/app")
    public Map app() {
        Map all = new HashMap();
        Set keys = new HashSet<>();
        try {
            Resource resource = resourceLoader.getResource("classpath:application.properties");
            InputStream is = resource.getInputStream();
            Properties prop = new Properties();
            prop.load(is);
            keys = prop.keySet();
            for(Object key:keys) {
                all.put((String) key, env.getProperty((String) key));
            }
        }
        catch (IOException e) {
            log.error(e.getMessage(), e);
        }
  
        return all;
    }
} 
下面是建立 pod 的 yaml,第 9~13 行就是傳入環境變數的方式。
apiVersion: v1
kind: Pod
metadata:
  name: practice-pod
spec:
  containers:
  - name: practice-pod
    image: steven/practice-pod
    env:
    - name: spring_profiles_active
      value: "test"
    - name: jasypt_encryptor_password
      value: "mykey"
  imagePullSecrets:
  - name: steven-harbor
如果我們用 ReplicaSet 啟動兩個 pod,如下:
practice-pod 這個 pod 是用上面的 yaml 產生的,確實可以取得環境變數,但是 practice-rs-grtx7 和 practice-rs-lncdr 這兩個 pod 並無法取得環境變數,因為 rs 每次產生 pod,是從 image 產生,並沒有傳入環境變數,為了解決這個問題,建立 ReplicaSet 的 yaml 要如下 18~22 行:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: practice-rs
spec:
  replicas: 2
  selector:
    matchLabels:
      app: practice-rs
  template:
    metadata:
      labels:
        app: practice-rs
    spec:
      containers:
      - name: practice-pod
        image: harbor.steven.idv.tw/steven/practice-pod
        env:
        - name: spring_profiles_active
          value: "test"
        - name: jasypt_encryptor_password
          value: "mykey"
      imagePullSecrets:
      - name: steven-harbor
輸入如下指令測試看看: (使用 kubectl get svc 指令,可以看到 external-ip)
curl http://172.16.3.39:9090/info/app
輸出如下結果:
{"jasypt.encryptor.algorithm":"PBEWithMD5AndDES","jasypt.encryptor.bean":"encryptorBean","server.servlet.context-path":"/","server.port":"8080","jasypt.encryptor.password":"mykey","spring.profiles.active":"test"}
可以看到,兩個環境變數都有抓到。

2020年4月14日 星期二

K8s: Ingress

繼續前面「K8s: Service」的例子,建立好服務後,服務會有對內的 cluster IP 及對外的 external IP,這樣對外服務是沒有問題了,但是,一個 Kubernetes 裡會有無數個服務,外部系統要使用這些服務有沒有類似 API Gateway 的功能呢? Kubernetes 提供的解答是 Ingress ! 它就是提供了一些基本功能的 API Gateway。
如上圖,Ingress 會根據網址導向不同的 service,以「K8s: Service」的例子,這裡寫了如下的 yaml 檔來產生 Ingress。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: steven-gw
spec:
  rules:
  - host: steven-gw.steven.idv.tw
    http:
      paths:
      - path: /host/info
        backend:
          serviceName: host-http
          servicePort: 9090
  1. 第 4 行定義了 Ingress 的名稱
  2. 第 7 行定義當 Ingress 收到這個網址的 request,會將 request 以下面的規則導向服務。
  3. 第 10 行定義,當網址開頭為 /host/info 導向 host-http 服務,port 為 9090,這是上一篇創建服務時,服務提供出來的 port。
執行上面的 yaml 後,用如下指令看一下 Ingress 綁定到那一個 IP:
kubectl get ingresses
應該會出現如下結果:
有了 IP 後,就可以在 DNS 裡設定 steven-gw.steven.idv.tw 指向該 IP,或是在 /etc/hosts 中設定,接下來就可以用如下指令測試看看。
curl http://steven-gw.steven.idv.tw/host/info
【番外篇】
上面是在 VMware PKS 上測試,PKS 上有預設安裝了 Ingress Controller,所以 ADDRESS 欄會有 IP,如果是自行安裝的 Kubernetes Cluster,因為 K8s 只提供 Ingress API 的定義,實作要再另外安裝,目前比較常見的有三種選擇:
  1. Nginx Kubernetes
  2. F5 BIG-IP Controller
  3. Ingress Kong
底下是 Ingress Kong 的安裝指令。
$ helm repo add kong https://charts.konghq.com
$ helm repo update
$ helm install kong/kong --generate-name --set ingressController.installCRDs=false

2020年4月10日 星期五

安裝 Harbor (Ubuntu)

Docker 官方的 docker registry 是 Docker Hub,以金融業來說,不可能將 docker image 放到公司外部,自建一個私有的 docker registry 就成為必要的需求,Harbor 是許多選項之一,也是目前最流行的選項之一,底下是安裝的步驟。
  • 下載 & 解壓縮
    • git hub 上下載
    • 解壓縮: tar xzvf harbor-offline-installer-v1.10.2.tgz
    • 我的 home 目錄是 /home/steven,解壓縮後檔案在 /home/steven/harbor。
    • cd harbor
  • 設定 hosts
  • 因為我希望 Harbor 的網址是 harbor.steven.idv.tw,所以在 /etc/hosts 中定義了:
    192.168.0.120 harbor.steven.idv.tw
    
  • 產生憑證
  • openssl req -newkey rsa:4096 -nodes -sha256 -keyout ca.key -x509 -days 3650 -out ca.crt
    
    openssl req -newkey rsa:4096 -nodes -sha256 -keyout harbor.steven.idv.tw -out harbor.steven.idv.tw
    
    建立一個名為 extfile.cnf 的網案,內容如下:
    subjectAltName = IP:192.168.0.120
    
    繼續產生憑證:
    openssl x509 -req -days 3650 -in harbor.steven.idv.tw -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.cnf -out harbor.steven.idv.tw
    openssl req -new -x509 -text -key ca.key -out ca.cert
    
    接著將產生的三個檔案 copy 到以下目錄 (沒有這個目錄就自己建立),不只 master node,所有 worker node 的該目錄也都要有這些檔案。
    sudo cp *.crt *.key *.cert /etc/docker/certs.d/harbor.steven.idv.tw
  • 修改 harbor.yml
  • hostname: harbor.steven.idv.tw
    port: 80
    certificate: /etc/docker/certs.d/harbor.steven.idv.tw/ca.crt
    private_key: /etc/docker/certs.d/harbor.steven.idv.tw/ca.key
    
  • 安裝 Harbor
  • 執行 sudo ./install.sh --with-clair,執行過程沒有錯誤的話,打開瀏覽器,輸入 http://harbor.steven.idv.tw 應該可以看到如下畫面。(安裝前請先確定已經有安裝 docker-compose)
    預設的帳號、密碼是 admin / Harbor12345,登入後畫面如下,點選 Administrator > Users,,按【+ NEW USER】新增一個 user 命名為 steven。
    接著在「Projects」中新增一個命名為 home 的 project。
    點選 home 於「Members」將剛剛新建立的 user steven 加入,這樣 steven 才能存取 home。
  • 測試
  • 測試前先修改 /etc/docker/daemon.json,如果沒有這個檔案,自行創建,在裡面加入如下內容:
    {"insecure-registries": ["192.168.0.120"]}
    
    然後重啟 docker
    systemctl restart docker
    
    登入 docker (steven / P@ssw0rd)
    docker login harbor.steven.idv.tw
    
    因為目前 docker 裡有個 image harbor.steven.idv.tw/home/host,如下:
    所以可以用以下指令將 image 推送到 harbor 上。
    docker push harbor.steven.idv.tw/home/host
    
    檢查一下 harbor,確實已經推上去了,如下圖。
  • 啟動 & 關閉
  • sudo docker-compose up -d
    sudo docker-compose down