2019年5月15日 星期三

將 windows server 加入 kubernetes cluster 成為 worker node

k8s 官方文件

ms 官方文件

kubernetes 原本環境的調整

  • CNI 限定 flannel (其實有三種,只是裡面最常見的是 flannel,官網範例也是用 flannel)
    flannel 的設定要照官網上的建議修改 net-conf.json 及 cni-conf.json 的內容
  • 修改 DaemonSet 的 yaml ,包含 kube-proxy、kube-flannel-ds-amd64、metallb 等
    spec.template.spec.nodeSelector 加上 beta.kubernetes.io/os=linux

    既有的 DaemonSet / Deployment / StatefulSet 等等,全都要做類似的修改。
    之後新增的也都要加上 nodeSelector。


windows server 2019 上的設定 (後面的操作全都是透過 PowerShell )

  • 安裝 docker

Install-Module -Name DockerMsftProvider -Repository PSGallery -Force
Install-Package -Name Docker -ProviderName DockerMsftProvider                            Restart-Computer -Force
Start-Service docker

  • 取得 kubernetes 的 certificate file

mkdir c:\k
cd c:\k
scp root@[master ip]:/root/.kube/config c:\k\config

  • 取得 windows node 上需要的 binary file,例如 kubelet、kube-proxy 等
    這邊兩個官網給的說明都有點不太清楚,不知道為什麼不直接提供連結
    找 node binaries 裡的 kubernetes-node-windows-amd64.tar.gz

wget -O c:\k\node.tar.gz https://dl.k8s.io/v1.13.6/kubernetes-node-windows-amd64.tar.gz
tar zxvf node.tar.gz
cd kubernetes\node\bin\
cp kube-proxy.exe c:\k\
cp kubelet.exe c:\k\
cp kubectl.exe c:\k\
      為什麼不一次 cp 三個檔案? 因為 powershell 裡就是沒辦法一次 cp 多個檔案,可能要加參數,不過我懶得查....


  • 這邊有一段是 kubernetes 官網上沒有但是 MS 官網有的環境變數設定

$env:Path += ";C:\k"
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\k",[EnvironmentVariableTarget]::Machine)
$env:KUBECONFIG="C:\k\config"                                                                     
[Environment]::SetEnvironmentVariable("KUBECONFIG", "C:\k\config", [EnvironmentVariableTarget]::User)

  • 下載 windows 上的 flannel binary file

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
wget https://raw.githubusercontent.com/Microsoft/SDN/master/Kubernetes/flannel/start.ps1 -o c:\k\start.ps1

  • 用 start.ps1 script 啟動 flannel 並帶起 kubelet 及 kube-proxy
  • 如果 flannel 設定都是預設值的話,指令大約如下
.\start.ps1 -ManagementIP -NetworkMode overlay -ClusterCIDR 10.244.0.0/16 -ServiceCIDR 10.96.0.0/12 -KubeDnsServiceIP 10.96.0.10 -LogDir c:\k

剩下是測試驗證了,照 MS 官網的文章,和一般 kubernetes deploy 的方式一樣
不過測試用的 image 感覺下載很慢,可以先用 docker pull 下來後,再跑測試會比較方便一些。
 

2019年5月8日 星期三

zero downtime in kubernetes

這是 原始文章,這篇不是翻譯,算是個人解讀後的摘要,如果有誤請指正~
原文有更詳細的解說,尤其第四篇文章內有關於 PDB 機制圖文並茂的解說。
要注意的是,這篇講的 zero downtime,指的是因某些需求而必須要主動停機的情況,如何在主動停機時不會中斷服務。


* 定義所遇到的挑戰為何
   假設目前有兩台 worker node,上面已經在跑一些服務。如果想要升級 worker node 的 kernel 時應該如何進行?
   一個方式是先裝新的 worker node join cluster,然後把原本 worker node 關機。           

   這會產生哪些考量?
  a.  假如這個 pods 需要在結束前做一些清理動作,但是關機程序沒有時間讓 pods 完成這些動作。
  b.  假如所有原本的 nodes  一起關機,會產生一段時間的服務中斷。

  首先要如何避免讓 pods reschedule 到原本的 pods 上?
  用 kubectl drain 設定原本的 node,確認所有的 pods 都移走後再關機。

  不過用 drain 的方式還是有兩個因素會造成服務中斷:
  a.  pods 裡的 applicaion 要能處理 TERM signal,因為 kubectl drain 是送 TERM signal 來 evicted pods。
       kubernetes 會在送出 TERM signal 後的一定時間內,去強制結束掉還在執行的 container。
  b.  如果 pods 在新的 node 起來前,舊的 node 上的 pods 就先都被終止,會產生服務中斷。

  Kubernetes 提供了三種機制來處理主動中斷 (例如 drain) 的情況
  a. Graceful termination
  b. Lifecycle hooks
  c. PodDisruptionBudgets


* Gracefully shutting down Pods

  kubectl drain 讓 kubelet 去 delete pod 的流程
  a. kubectl drain 發出通知給指定的 node,讓該 node 的 kubelet 開始準備做 delete pods 的動作
  b. kubelet 會執行 pod 定義裡的 preStop 設定
  c. preStop 執行完後,kubelet 會送出 TERM signal 給 pod
  d. kubelet 送出 TERM signal 後,會等待一個時間 (grace period, 預設是 30 秒) 後強制刪除 pod
 
 也就是在 kubelet 強制刪除 pod 前,有兩個時間點可以讓 pod 做清理的動作。
 例如說,可以設定應用程式收到 TERM signal 時停止接受新的工作,並且在完成所有已收到的工作後自行停止。

  如果沒辦法修改應用程式去處理 TERM signal ,可以設定 pod 的 preStop 去終止應用程式。

  不過在後面這種情況下,應用程式雖然可以透過 preStop 停止,但是 service 那邊在可能還不知道的情況下(見下節說明),還會繼續把 request 往 pods 送過去,使用者就會看到錯誤或服務被拒絕的結果。

* Delaying Shutdown to Wait for Pod Deletion Propagation

  回頭來看,當送出 delete pods 時,整個 kubernetes cluster 會進行哪些處理?
  a. kubelet 會開始進行 delete pod 的流程,如同前一節描述的
  b. 所有 node 上 kube-proxy 會移除該 pod ip 相關的 iptables rules
  c. endpoints controller 會移除和該 pod 相關的 endpoint,也就是會從 Service 中移除該 pod
 
  這就是為什麼透過 preStop 終止 container 後,流量 (request) 還可能會繼續往 pods 送的原因。

  一種應對方式是,確保 kube-proxy 和 endpoints controller 都處理完後,再終止 container。
  不過這方式是非常困難的,因為 node 可能有網路延遲,或者 node 數量非常非常多。

  實際上沒有辦法做到 100% 服務不中斷,只能盡可能做到 99%。
  方法是在 preStop 用 sleep 指令延後終止 container 的動作,在 "Kubernetes in Action" 一書中建議 sleep 的時間是 5~10 秒。

  也就說,如果在這 5~10 秒內,endpoints controller 可以移除相關的 endpoint,就可以在 container 終止前停止把流量送過去,也就不會有中斷服務的情況。

  此時 Deployment 會因為要保持 replicas 設定的數量在新的 node 建立新的 pods。         
  接下來的問題是,假如同時 drain 所有原本的 node,可能產生的問題是,在新的 pods 產生前,所有原本的 pods 都終止了,而造成了服務中斷。

  如果不要同時,而是逐步 drain node 會遇到什麼問題?
  第一台終止的 pod ,可能被安排到另一台原本也要停機的 node 上。也就是該 Deployment 的兩個 replicas 都在同一台準備要停機的 node 上。 當輪到這台 node 要 drain 時,就會造成這個 deployment 的 pods 全都準備終止的情況。下一節會來處理這個處理這個問題。


* Avoiding Outages in your Kubernetes Cluster using PodDisruptionBudgets

    使用 PDB (PodDistruptionBudgets) 這個機制來處理上節最後面提到的問題。
    PDB 是用來設定對於終止 pod 行為的容忍機制。

    如果在 PDB 的 yaml 設定 minAvailable: 1,表示至少要有一個 pod 存活。這可以防止所有 pods 全部一起被 delete 。在上述情境中,會等到新的 node 裡至少有一個 pod 起來後,才會讓原本最後一個 node 裡的 pod 被刪除。


綜合以上幾個設定方式,可以達到 zero downtime 的目標。

2019年5月7日 星期二

用 ansible 架 kubernetes cluster

用 ansible 架 kubernetes 不是新鮮事,成熟的專案有 kubespray,而 openshift 也是用 ansible 安裝。

會接觸 ansible 並用來裝 kubernetes cluster ,在於我想要整合用 ansible 去建立 vm (base on kvm),vm 是使用預先建立好的 CentOS 7 的 image。

因為是 vm 環境, kubespray 就顯得太龐大了,因此決定自己來寫一個 kvm 版本的安裝流程,從 virsh 建立 vm 開始,到設定 vm 網路環境,然後建立 kubernetes cluster。

這過程花了大約兩個禮拜的時間完成,只能說 ansible 真是一個很容易陷入的工具,因為太容易上手並取得成就感了。兩個禮拜裡從基本的 playbook 到建立 roles ,不斷地重構原本的程式並加入新的功能,以及嘗試讓設定更有彈性。雖然對 ansible 掌握度還不夠,但已經足夠完成目標想做到的 (基本) 程度了。

透過 inventory 裡的設定,可以建立符合需求數量的 vm ,安裝 haproxy + kubernetes cluster + harbar ,以及 kuberntes 一些基本的環境 (metallb + nginx-ingress + helm + metrics-server),harbar 也整合到 kuberentes cluster 。

對我來說,ansible 最難掌握的就是變數的傳遞,花了最多時間去查資料及適應 ansible 的邏輯。 在不同 playbook 間要傳遞變數,我目前知道的方式是 add_host,透過把變數放到虛構的 host 裡,因為 hostvars 相當於一個 global 的變數。另外就是不同方式設定變數的 priority,沒有弄清楚前也吃了一點苦頭。

因為 vm image 是可以自訂的,所以可以先把一些必要的東西先埋進去,這讓 ansible 在處理上也簡單不少,這算是用 vm 來處理的一個優勢。對於已經有 kvm 環境的人來說,只要硬體資源足夠,預先件好需要的 image 後,用 ansible 搭一個上述的 kuberentes 環境,大約只需要 3~5 分鐘左右的時間。

ansible 之旅暫時先告一段落,先回頭看 kubernetes 相關的工具與管理,等到對於 kubernetes 維護管理層面更完整後,再來修改 ansible,目標是可以在 kvm 環境裡快速搭建一個完整且實用的 kuberentes 。


kubernetes cluster 安裝

從 kubernetes 1.14 之後,越來越容易用 kubeadm 安裝 cluster 了,基本的環境準備好之後,熟練的話可能十分鐘就裝好一個 kuberentes cluster (master * 3 + worker * 3)

為了簡化設定環境,可以把六台的 selinux 和 firewall 都先關掉。
安裝環境假設準備如下:

  1. 六台機器, OS 為 CentOS 7
    假設其 ip 為 192.168.1.11 ~ 192.168.1.16
  2. 選定一台當第一台 master,設定 master 可以直接以 ssh key 登入其他五台
    假設第一台 master 為 192.168.1.11
    另外兩台 master 分別是 192.168.1.12 及 192.168.1.13
  3. 準備一台機器設定好 haproxy ,做為三台 master node 的前端 proxy
    假設 haproxy 的 ip 為 192.168.1.10
  4. 六台機器都準備以下文件 (檔案內容放在文後)
    * /etc/yum.repos.d/kubernetes.repo
    第一台 master 準備以下文件 (檔案內容放在文後)
    * /tmp/kube-config.yaml
  5. 六台先進行如下設定
    # yum update && yum install -y yum-utils
    # yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    # yum install -y docker-ce kubelet kubeadm kubectl --disableexcludes=kubernetes
    # systemctl enable --now docker
    # systemctl enable --now kubelet
    # swapoff -a
    * modprobe br_netfilter

到 192.168.1.11 (第一台 master) 開始安裝
# kubeadm init --config=/tmp/kube-config.yaml --experimental-upload-certs

一行指令就安裝完成了。

安裝完成後會提供一些訊息,其中有兩行分別是提供給其他 master 或 worker 加入 cluster,訊息內容類似如下:

kubeadm join 192.168.1.10:6443 --token 9vr73a.a8uxyaju799qwdjv --discovery-token-ca-cert-hash sha256:7c2e69131a36ae2a042a339b33381c6d0d43887e2de83720eff5359e26aec866 --experimental-control-plane --certificate-key f8902e114ef118304e561c3ecd4d0b543adc226b7a07f675f56564185ffe0c07
將這個指令複製下來到 192.168.1.12 及 192.168.1.13 上面執行,就可以將這兩台以 master 身份加入到 cluster 
kubeadm join 192.168.1.10:6443 --token 9vr73a.a8uxyaju799qwdjv --discovery-token-ca-cert-hash sha256:7c2e69131a36ae2a042a339b33381c6d0d43887e2de83720eff5359e26aec866
將這個指令複製下來到另外三台上面執行,就可以將這三台以 worker 身份加入到 cluster

要注意的是,兩個指令裡的 token 及 key 是有使用期限的,時間到了 token 和 key 就會被移除。所以如果一段時間後要將新的機器加
入 cluster ,就需要透過 kubeadm 指令重新產生 token 和 key 來使用。

不過在將其他台機器加入 cluster 之前,還有一個動作要先做,要指定及安裝 kubernetes cluster 內部使用的網路模式 (CNI),這邊以 weave 為範例,在第一台 master 上執行以下指令

# kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"


最後附上需要的設定檔

/etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kube*

* /tmp/kube-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: stable
controlPlaneEndpoint: "192.168.1.10:6443"
networking:
  podSubnet: 10.244.0.0/16

Dockerfile Best Practice

來源是從這份 簡報

這份簡報主要提出改進的目標如下:
  • build time
  • image size
  • maintainability
  • security
  • consistency/repeatability
簡報開頭先建議使用 buildkit 功能
先摘錄一下官網路個人認為比較重要的部分:
  • docker build 新選項
    --secret 可以避免在 Dockerfile 中直接寫入密碼
    --ssh 可以讓 build 出來的 image 直接有 ssh daemon
  • 使用 buildkit
    從環境變數打開 : DOCKER_BUILDKIT=1 docker build .
    修改設定檔: /etc/docker/daemon.json : { "features": { "buildkit": true } }

然後簡報開始針對上面的目標提出改善方式
  1. 減少 build time
    a. 善用 cache ,把比較容易變動的指令放到後面
    b. 指定 copy 的範圍可以減少 cache 使用,例如 
        COPY . /app  改成 COPY target/app.jar /app
    c. 避免使用到過期的套件,例如將 apt-get update 和 install 合併為一行
        apt-get update && apt-get -y install ssh vim
  2. 減少 image size
    a. 移除非必要的套件, apt-get -y install --no-install-recommends
    b. 移除安裝套件的 cache , rm -fr /var/lib/apt/lists/*
  3. 維護性
    a. 盡量使用官方出的 image
    b. 盡量指定 image 的 tag ,例如 FROM openjdk:8
    c. 從官網尋找符合需求下 size 最小的 image tag
  4. 重複使用性
    a. 盡量維持 build 環境的一致性
    b. 透過 multi-stage build 來減少 build iamge 時的相依性

簡報提出 multi-stage 的適用情境
  • separate build from runtime envrionment ( 降低 image size )
  • 降低 image 的變動
  • Build /dev /test /lint /... specific envrionments
  • 將相依性變成非線性化 (concurrency)
  • Platform-specific stages

使用 multi-stage 情境的範例
  • 用 --target 來指定要 build 的 stage

    Dockerfile:
    From image_or_stage AS stage_name

    Build Command: 
    docker build --target stage_name
  • use global Arguments

    Dockerfile:
    ARG flavor=alpine
    FROM openjdk:8-jre-$flavor AS release

    Build Command:
    docker build --target release --build-arg flavor=jessie .
  • buildkit 可以在 build 時排除非必要的 stage,並且可以多個 stage 同時進行 build

Dockerfile 新的語法 (experimental) ,以下語法都是在 docker v18.09+ w/ BuildKit 使用
  • context mounts
    COPY . /app => RUN --mount=target=. mvn -e -B package -DoutputDirectory=/
  • cache dependencies
    RUN --mount=target=. --mount=type=cache,target=/root/.m2 && mvn package -DoutputDirectory=/
  • serects

    錯誤的方法:
    (1) ENV AWs-ACCESS_KEY_ID  (直接寫在 Dockerfile)
    (2) ARG AWs-ACCESS_KEY_ID  (會保存在 docker history)

    建議的方式:
    Dockerfile:
    RUN --mount=type=secret,id=aws,target=/root/.aws/credentials,required

    Build Command:
    docker build --secret id=aws,src=~/.aws/credentials .
  • 使用 private git repos

    錯誤的方法:
    COPY ./keys/private.pem /root/.ssh/private.pem

    建議的方式:
    Dockerfile:
    RUN --mount=type=ssh,required git clone git@xxx:/repo /work

    Build Command:
    docker build --ssh=default .